lewis
lewis

Reputation: 1263

How do I subclass a JSON schema

How do I subclass in JSON-Schema?

First I restrict myself to draft-07, because that's all I can find implementations of.

The naive way to do sub-classing is described in

https://json-schema.org/understanding-json-schema/structuring.html#extending

But this works poorly with 'additionalProperties': false?

Why bother with additionalProperties': false?

Without it - nearly any random garbage input json will be considered valid, since all the 'error' (mistaken json) will just be considered 'additionalProperties'.

Recapping https://json-schema.org/understanding-json-schema/structuring.html#extending

The problem with this - is that it doesn't work with 'additionalProperties' (because of unclear but appantly unfortunate definitions of additionalProperties that it ONLY applies to locally defined (in that sub-schema) properties, so one or the other schema will fail validation.

Alternative Approaches:

Upvotes: 1

Views: 8423

Answers (3)

Ivan Gabriele
Ivan Gabriele

Reputation: 6900

I struggled with that, especially since I had to use legacy versions of JSON Schema. And I found that the solution is a tiny bit verbose but quite easy to read and understand.

Let's say that you want describe that kind of type:

interface Book {
  pageCount: number
}

interface Comic extends Book {
  imageCount: number
}

interface Encyclopedia extends Book {
  volumeCount: number
}

// This is the schema I want to represent:
type ComicOrEncyclopedia = Comic | Encyclopedia

Here is how I can both handle polymorphism and forbid any extra-prop (while obviously enforcing inherited types in the "child" definitions):

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "bookDefinition": {
      "type": "object",
      "properties": {
        "imageCount": {
          "type": "number"
        },
        "pageCount": {
          "type": "number"
        },
        "volumeCount": {
          "type": "number"
        }
      }
    },

    "comicDefinition": {
      "type": "object",
      "allOf": [{ "$ref": "#/definitions/bookDefinition" }],
      "properties": {
        "imageCount": {},
        "pageCount": {},
        "volumeCount": {
          "not": {}
        }
      },
      "required": ["imageCount", "pageCount"],
      "additionalProperties": false
    },
    "encyclopediaDefinition": {
      "type": "object",
      "allOf": [{ "$ref": "#/definitions/bookDefinition" }],
      "properties": {
        "imageCount": {
          "not": {}
        },
        "pageCount": {},
        "volumeCount": {}
      },
      "required": ["pageCount", "volumeCount"],
      "additionalProperties": false
    }
  },
  "type": "object",
  "oneOf": [
    { "$ref": "#/definitions/comicDefinition" },
    { "$ref": "#/definitions/encyclopediaDefinition" }]
}

Upvotes: 5

Jason Desrosiers
Jason Desrosiers

Reputation: 24409

How do I subclass in JSON-Schema?

You don't, because JSON Schema is not object oriented and schemas are not classes. JSON Schema is designed for validation. A schema is a collection of constraints.

But, let's look at it from an OO perspective anyway.

Composition over inheritance

The first thing to note is that JSON Schema doesn't support an analog to inheritance. You might be familiar with the old OO wisdom, "composition over inheritance". The Go language, chooses not to support inheritance at all, so JSON Schema is in good company with that approach. If you build your system using only composition, you will have no issues with "additionalProperties": false.

Polymorphism

Let's say that thinking in terms of composition is too foreign (it takes time to learn to think differently) or you don't have control over how your types are designed. For whatever reason, you need to model your data using inheritance, you can use the allOf pattern you're familiar with. The allOf pattern isn't quite the same as inheritance, but it's the closest you're going to get.

As you've noted, "additionalProperties": false wreaks havoc in conjunction with the allOf pattern. So, why should you leave this out? The OO answer is polymorphism. Let's say you have a "Person" type and a "Student" type that extends "Person". If you have a Student, you should be able to pass it to a method that accepts a Person. It doesn't matter that Student has a few properties that Person doesn't, when it's being used as a Person, the extra properties are simply ignored. If you use "additionalProperties": false, your types can't be polymorphic.


None of this is the kind of solution you are asking for, but hopefully it gives you a different perspective to consider alternatives to solve your problem in different way that is more idiomatic for JSON Schema.

Upvotes: 2

lewis
lewis

Reputation: 1263

This isn't a GREAT answer. But until the definition of JSONSchema is improved (or someone provides a better answer) - this is what I've come up with as workable.

Basically, you define two copies of each type, the first with all the details but no additionalProperties: false flag. Then second, REFERENCING the first, but with the 'additionalProperties: false' set.

The first you can think of as an 'abstract class' and the second as a 'concrete class'.

Then, to 'subclass', you use the https://json-schema.org/understanding-json-schema/structuring.html#extending approach, but referencing the ABSTRACT class, and then add the 'additionalProperties: false'. SADLY, to make this work, you must also REPEAT all the inherited properties (but no need to include their type info - just their names) - due to the sad choice for how JSONSchema draft 7 appears to interpret additionalProperties.

An EXAMPLE - based on https://json-schema.org/understanding-json-schema/structuring.html#extending should help:

https://www.jsonschemavalidator.net/s/3fhU3O1X

(reproduced here in case other site /link not permanant/reliable)

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://TEST",
  "definitions": {
    "interface-address": {
      "type": "object",
      "properties": {
        "street_address": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "state": {
          "type": "string"
        }
      },
      "required": ["street_address", "city", "state"]
    },
    "concrete-address": {
      "allOf": [
        {
          "$ref": "#/definitions/interface-address"
        }
      ],
      "properties": {
        "street_address": {},
        "city": {},
        "state": {}
      },
      "additionalProperties": false
    },
    "in-another-file-subclass-address": {
      "allOf": [
        {
          "$ref": "#/definitions/interface-address"
        }
      ],
      "additionalProperties": false,
      "properties": {
        "street_address": {},
        "city": {},
        "state": {},
        "type": {
          "enum": ["residential", "business"]
        }
      },
      "required": ["type"]
    },
    "test-of-address-schemas": {
      "type": "object",
      "properties": {
        "interface-address-allows-bad-fields": {
          "$ref": "#/definitions/interface-address"
        },
        "use-concrete-address-to-only-admit-legit-addresses-without-extra-crap": {
          "$ref": "#/definitions/concrete-address"
        },
        "still-can-subclass-using-interface-not-concrete": {
          "$ref": "#/definitions/in-another-file-subclass-address"
        }
      }
    }
  },
  "anyOf": [
    {
      "$ref": "#/definitions/test-of-address-schemas"
    }
  ]
}

and example document:

{
   "interface-address-allows-bad-fields":{
      "street_address":"s",
      "city":"s",
      "state":"s",
      "allow-bad-fields-this-is-why-we-need-additionalProperties":"s"
   },
   "use-concrete-address-to-only-admit-legit-addresses-without-extra-crap":{
      "street_address":"s",
      "city":"s",
      "state":"s"
   },
   "still-can-subclass-using-interface-not-concrete":{
      "street_address":"s",
      "city":"s",
      "state":"s",
      "type":"business"
   }
}

Upvotes: 2

Related Questions