Reputation: 1263
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
use allOf(baseClass)
then add your own properties
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:
meta languages/interpretters layered on top of JSONSchema (such as https://github.com/mokkabonna/json-schema-merge-allof)
This is not a good choice as the scehma can only be used from javascript (or the language of that meta processor). And not easily interoperable with other tools
https://github.com/java-json-tools/json-schema-validator/wiki/v5%3A-merge
An alternative I will propose as a 'solution' / answer
Upvotes: 1
Views: 8423
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
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.
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
.
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
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