50

I'm using JSON schema for data modelling. I define a base Document schema, that I later use to define model schemas (e.g. Product, Category, User, etc.).

I'm doing this because I want all models to inherit certain structure/rules. For example every model instance should have certain common properties (such as, id, createdAt, updatedAt). In OOP terminology: Product extends Document and therefore it inherits its instance properties. In schemas terminology (I think) Document is a meta-schema for creating model schemas.

I've defined the Document schema as follows:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "id": "http://example.com/schemas/document.json#",
  "title": "Document",
  "type": "object",
  "additionalProperties": false,
  "required": ["type", "name", "fields"],
  "properties": {
    "type": {
      "constant": "document"
    },
    "name": {
      "type": "string"
    },
    "title": {
      "type": "string"
    },
    "description": {
      "type": "string"
    },
    "readOnly": {
      "type": "boolean"
    },
    "properties": {
      // common properties 
      // model-specific properties
    }
  }
}
  1. How do I specify that the Document meta-schema "extends" the base JSON schema (draft-07), so that I don't have to define all the properties of the draft ($schema, id, etc.)?
  2. How do I specify that the properties of each model schema contains some common properties (id, createdAt, ...), without having to define them in each model schema definition?
Panagiotis Panagi
  • 9,927
  • 7
  • 55
  • 103

2 Answers2

97

JSON Schema doesn't use an object oriented paradigm, so concepts like inheritance don't translate well. JSON Schema is a collection of constraints. It's subtractive rather than additive like most people are used to. This means that given an empty schema, the set of valid JSON documents is the set of all JSON documents. As you add keywords, you are subtracting from the set of valid JSON documents. Once something is removed from the set, it can't be added back in.

Therefore, you can use composition to "extend" a schema, but you can never "override" something that another schema defines.

Let's look at a simple extension example with no conflicting properties.

/schema/base

{
  "type": "object",
  "properties": {
    "foo": { "type": "string" },
    "bar": { "type": "string" }
  }
}

/schema/extended

{
  "allOf": [{ "$ref": "/schema/base" }],
  "properties": {
    "baz": { "type": "string" }
  }
}

That works great with JSON Schema. Now let's look at an example with conflicting property definitions.

/schema/override

{
  "allOf": [{ "$ref": "/schema/base" }],
  "properties": {
    "bar": { "type": "integer" },
    "baz": { "type": "boolean" }
  }
}

In this example, both schemas have a /properties/bar field. If you are thinking about this in terms of inheritance, you're going to misunderstand what is happening here. In this case, both "/properties/bar" fields must be valid. There is no conflict to resolve. As the keyword says, "all of" the schemas must be valid. Since bar can't possibly be both an integer and a string, no document will ever validate against the /schema/override.

Hopefully that gives you enough information to solve your problem and avoid the most common gotcha.

Jason Desrosiers
  • 22,479
  • 5
  • 47
  • 53
  • 3
    When you write "In this case, both "/properties/bar" fields must be valid", do you mean that `bar` must both be a string (according to the base schema) *and* be an integer (according to the override schema)? Since that is impossible any JSON will fail against the override schema, won't it? – md2perpe Aug 30 '19 at 11:49
  • 8
    That's correct. One doesn't take precedence over the other. They both apply. Since there is no JSON document that can possibly be both a string and an integer, then nothing will ever be valid against that schema. – Jason Desrosiers Aug 30 '19 at 17:09
  • `required` works the same as `properties` (or any other keyword). It's just added to the bag of constraints whether it comes from the base schema or the extending schema. If there are multiple `required`s, they all apply. (Maybe you meant, how does this work with `"additionalProperties": false`? That's where things get weird.) – Jason Desrosiers Jun 11 '20 at 23:03
  • @JasonDesrosiers in your examples, shouldn't the `properties` object be inside the `allOf` array? That's how it's done in the [example](https://json-schema.org/understanding-json-schema/reference/combining.html#allof) in json-schema.org. – waldyrious Aug 19 '20 at 18:50
  • @waldyrious The two styles are equivalent. I prefer this style because it reads nicer and produces better error messages. – Jason Desrosiers Aug 19 '20 at 19:17
  • 2
    It should be noted that the official guide specifically calls out this recommendation as not _actually_ extending anything -- https://web.archive.org/web/20201020173257/https://json-schema.org/understanding-json-schema/reference/combining.html#id5 – James Sumners Dec 01 '20 at 13:33
  • Why not just `"$ref": "/schema/base"` instead of `"allOf": [{ "$ref": "/schema/base" }]`? – Patrick Sep 25 '21 at 16:11
  • 2
    @Patrick If you are using draft 2019-09 or higher, then you could leave out the `allOf`. In earlier drafts of JSON Schema, if an object had a `$ref` in it, it was considered just a Reference and not a Schema. That means any JSON Schema keywords within that Reference get ignored because keywords only have meaning within a Schema. That effectively meant that `$ref` always had to be alone and we needed `allOf` to compose References and Schemas. With the new version of `$ref`, the Reference is the string value of the `$ref` keyword and `$ref` works like an `allOf` with one schema. – Jason Desrosiers Sep 26 '21 at 22:05
  • @JasonDesrosiers I see, thanks. I'd like to use draft 2019-09 or higher but I didn't found any free editor support on Linux... – Patrick Sep 27 '21 at 08:19
  • How do you "extend" objects within a single schema file? – trusktr Dec 17 '21 at 16:23
  • @JasonDesrosiers I'm not sure that it's going to work. The schema you provided will validate any object, because its properties will be considered "additional". But once you add `"additionalProperties": false` into your schema, it stops validating even the target objects. See: https://www.jsonschemavalidator.net/s/1h7UVsIJ – Onkeltem Apr 06 '22 at 19:05
  • @JasonDesrosiers I updated the schema to get it working: https://www.jsonschemavalidator.net/s/CeWN3Xnd according to the docs here: https://json-schema.org/understanding-json-schema/reference/object.html#id6 But it leaves me confused and disappointed with its uglyness and inefficiency: so one has to re-type each and every property with some awkward `true` value? I'm not sure what it is about. – Onkeltem Apr 06 '22 at 19:18
  • 3
    @Onkeltem replace `additionalProperties` with `unevaluatedProperties` and it will work as you expect. – Jason Desrosiers Apr 07 '22 at 00:26
  • @JasonDesrosiers Could you please update the answer accordingly? Here is updated version: https://www.jsonschemavalidator.net/s/S4K0HsrS – Onkeltem Apr 07 '22 at 09:10
3

When using NodeJs, it is straightforward to get around this limitation on simple schemas with a little code using ajv validator like this:

function extendJsonSchema(baseSchema, extendingSchema) {
    let extendedSchema = Object.assign({}, extendingSchema);
    extendedSchema.properties = Object.assign(extendedSchema.properties, baseSchema.properties)
    extendedSchema.required = extendedSchema.required.concat(baseSchema.required)
    return extendedSchema
}


let baseSchema = require('./base.schema.json')
let extendingSchema = require('./extending.schema.json')

let extendedSchema = extendJsonSchema(baseSchema, extendingSchema)
const validate = ajv.compile(extendedSchema)

This solves my use-case at least.

Gudlaugur Egilsson
  • 2,420
  • 2
  • 24
  • 23