82

Suppose we have schema following schema (from tutorial here):

{
  "$schema": "http://json-schema.org/draft-04/schema#",

  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street_address": { "type": "string" },
        "city":           { "type": "string" },
        "state":          { "type": "string" }
      },
      "required": ["street_address", "city", "state"]
    }
  },

  "type": "object",

  "properties": {
    "billing_address": { "$ref": "#/definitions/address" },
    "shipping_address": {
      "allOf": [
        { "$ref": "#/definitions/address" },
        { "properties":
          { "type": { "enum": [ "residential", "business" ] } },
          "required": ["type"]
        }
      ]
    } 

  }
}

And here is valid instance:

{
      "shipping_address": {
        "street_address": "1600 Pennsylvania Avenue NW",
        "city": "Washington",
        "state": "DC",
        "type": "business"
      }
}

I need to ensure that any additional fields for shipping_address will be invalid. I know for this purpose exists additionalProperties which should be set to "false". But when I'm setting "additionalProprties":false as in the following:

"shipping_address": {
          "allOf": [
            { "$ref": "#/definitions/address" },
            { "properties":
              { "type": { "enum": [ "residential", "business" ] } },
              "required": ["type"]
            }
          ],
          "additionalProperties":false
        } 

I get a validation error (checked here):

[ {
  "level" : "error",
  "schema" : {
    "loadingURI" : "#",
    "pointer" : "/properties/shipping_address"
  },
  "instance" : {
    "pointer" : "/shipping_address"
  },
  "domain" : "validation",
  "keyword" : "additionalProperties",
  "message" : "additional properties are not allowed",
  "unwanted" : [ "city", "state", "street_address", "type" ]
} ] 

The question is: how should I to limit fields for the shipping_address part only? Thanks in advance.

spinkus
  • 7,694
  • 4
  • 38
  • 62
lm.
  • 4,033
  • 4
  • 25
  • 37

5 Answers5

106

[author of the draft v4 validation spec here]

You have stumbled upon the most common problem in JSON Schema, that is, its fundamental inability to do inheritance as users expect; but at the same time it is one of its core features.

When you do:

"allOf": [ { "schema1": "here" }, { "schema2": "here" } ]

schema1 and schema2 have no knowledge of one another; they are evaluated in their own context.

In your scenario, which many, many people encounter, you expect that properties defined in schema1 will be known to schema2; but this is not the case and will never be.

This problem is why I have made these two proposals for draft v5:

Your schema for shipping_address would then be:

{
    "merge": {
        "source": { "$ref": "#/definitions/address" },
        "with": {
            "properties": {
                "type": { "enum": [ "residential", "business" ] }
            }
        }
    }
}

along with defining strictProperties to true in address.


Incidentally, I am also the author of the website you are referring to.

Now, let me backtrack to draft v3. Draft v3 did define extends, and its value was either of a schema or an array of schemas. By the definition of this keyword, it meant that the instance had to be valid against the current schema and all schemas specified in extends; basically, draft v4's allOf is draft v3's extends.

Consider this (draft v3):

{
    "extends": { "type": "null" },
    "type": "string"
}

And now, that:

{
    "allOf": [ { "type": "string" }, { "type": "null" } ]
}

They are the same. Or maybe that?

{
    "anyOf": [ { "type": "string" }, { "type": "null" } ]
}

Or that?

{
    "oneOf": [ { "type": "string" }, { "type": "null" } ]
}

All in all, this means that extends in draft v3 never really did what people expected it to do. With draft v4, *Of keywords are clearly defined.

But the problem you have is the most commonly encountered problem, by far. Hence my proposals which would quench this source of misunderstanding once and for all!

fge
  • 119,121
  • 33
  • 254
  • 329
  • 1
    Thanks for detailed explanation. As I understand "strictProperties" and "merge" are part of v5 spec. Is there existing stable validators supporting v5? Thanks – lm. Apr 11 '14 at 09:59
  • 3
    Those two properties are not part of any spec - they are features that @fge is *proposing*, so there's no guarantee they'll be in any future version. – cloudfeet Apr 11 '14 at 10:07
  • 2
    @fge - it's kinda not obvious in your answer that the keywords are not part of a spec, but are your proposed extension. They way they are presented makes them seem like an official solution without any warning that v5 is in flux, which I think is misleading. – cloudfeet Apr 11 '14 at 14:25
  • There is not need to improve draft V3 to solve this problem with an elegant solution: http://stackoverflow.com/a/24365393/1480391 – Yves M. Jun 23 '14 at 12:03
  • @fge is still not clear to me how you can combine more than 2 schemas (i.e. 3 entire schemas) into one with $merge . For some reasons I thought allOf it's a kind of storage/embedding method for multiple schemas with the only rule being to not have conflicting properties. – themihai Jun 04 '15 at 19:10
  • Sadly it seems v5 spec only turned out to be a maintenance spec. https://github.com/json-schema/json-schema/wiki/v5-Proposals Is there any thing that would solve this issue in v6? – Peter Nov 08 '17 at 12:43
  • 45
    Currently at v7 and I don't see any `merge` or `strictProperties`. Is it now possible to do what the OP is talking about ? – Julian Honma Jan 05 '18 at 14:38
  • 2
    @JulianHonma January, 2019 `strictProperties` and `merge` are not part of spec draft v7. – Sebastian Barth Jan 26 '20 at 16:28
  • 2
    They're now at version [2019-09](https://json-schema.org/draft/2019-09/release-notes.html) and still no sign of improvements in this area. – Renato Jan 05 '21 at 09:41
  • 7
    From 2019-09 on there is "unevaluatedProperties" which can be set to false. This will finally solve the problem here. – Andreas H. Nov 09 '21 at 19:51
14

additionalProperties applies to all properties that are not accounted-for by properties or patternProperties in the immediate schema.

This means that when you have:

    {
      "allOf": [
        { "$ref": "#/definitions/address" },
        { "properties":
          { "type": { "enum": [ "residential", "business" ] } },
          "required": ["type"]
        }
      ],
      "additionalProperties":false
    }

additionalProperties here applies to all properties, because there is no sibling-level properties entry - the one inside allOf does not count.

One thing you could do is to move the properties definition one level up, and provide stub entries for properties you are importing:

    {
      "allOf": [{"$ref": "#/definitions/address"}],
      "properties": {
        "type": {"enum": ["residential", "business"]},
        "addressProp1": {},
        "addressProp2": {},
        ...
      },
      "required": ["type"],
      "additionalProperties":false
    }

This means that additionalProperties will not apply to the properties you want.

cloudfeet
  • 12,156
  • 1
  • 56
  • 57
  • Thanks for reply , but it was not help : here is error : [ { "level" : "error", "schema" : { "loadingURI" : "#", "pointer" : "/properties/shipping_address" }, "instance" : { "pointer" : "/shipping_address" }, "domain" : "validation", "keyword" : "additionalProperties", "message" : "additional properties are not allowed", "unwanted" : [ "city", "state", "street_address" ] } ] - same as before. – lm. Mar 28 '14 at 06:46
  • Did you import "city" "state" and "street_address", like "addressProp1" in my example? – cloudfeet Mar 28 '14 at 16:29
  • Can you clarify ? I changed schema according to your changes. "city" "state" and "street_address" remain part of referenced schema, did you mean it ? I checked with validator http://json-schema-validator.herokuapp.com/ – lm. Mar 28 '14 at 19:39
  • 1
    That's currently the only working solution, albeit rather cumbersome (when you have to add a dozen properties just to keep the `"additionalProperties": false` happy) – Julian Honma Jan 05 '18 at 15:06
  • Not working :/ . – John T Apr 15 '22 at 07:42
7

Here's a slightly simplified version of Yves-M's Solution:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street_address": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "state": {
          "type": "string"
        }
      },
      "required": [
        "street_address",
        "city",
        "state"
      ]
    }
  },
  "type": "object",
  "properties": {
    "billing_address": {
      "$ref": "#/definitions/address"
    },
    "shipping_address": {
      "allOf": [
        {
          "$ref": "#/definitions/address"
        }
      ],
      "properties": {
        "type": {
          "enum": [
            "residential",
            "business"
          ]
        },
        "street_address": {},
        "city": {},
        "state": {}
      },
      "required": [
        "type"
      ],
      "additionalProperties": false
    }
  }
}

This preserves the validation of required properties in the base address schema, and just adds the required type property in the shipping_address.

It's unfortunate that additionalProperties only takes the immediate, sibling-level properties into account. Maybe there is a reason for this. But this is why we need to repeat the inherited properties.

Here, we're repeating the inherited properties in simplified form, using empty object syntax. This means that properties with these names would be valid no matter what kind of value they contained. But we can rely on the allOf keyword to enforce the type constraints (and any other constraints) declared in the base address schema.

Community
  • 1
  • 1
Ted Epstein
  • 2,639
  • 20
  • 19
4

Since no one has posted a valid answer for spec 2019-09 and upwards and I almost missed Andreas H.'s comment;

{
  "$schema": "http://json-schema.org/2019-09/schema#",

  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street_address": { "type": "string" },
        "city":           { "type": "string" },
        "state":          { "type": "string" }
      },
      "required": ["street_address", "city", "state"]
      // additionalProperties: false    // <-- Remove completely if present 
    }
  },

  "type": "object",

  "properties": {
    "billing_address": { "$ref": "#/definitions/address" },
    "shipping_address": {
      "unevaluatedProperties": false,   // <-- Add to same level as allOf as false
      "allOf": [
        { "$ref": "#/definitions/address" },
        { "properties":
          { "type": { "enum": [ "residential", "business" ] } },
          "required": ["type"]
        }
      ]
    } 
  }
}

A pretty clear and succinct explanation can be found by the author here;

myol
  • 8,857
  • 19
  • 82
  • 143
  • This is the right idea but the code is invalid because `unevaluatedProperties` is only valid in draft 2019-09 or newer. this answer should be updated to indicate the proper `$schema` version – Jeremy Fiel Feb 09 '23 at 14:48
2

Don't set additionalProperties=false at definition level

And everything will be fine:

{    
    "definitions": {
        "address": {
            "type": "object",
            "properties": {
                "street_address": { "type": "string" },
                "city":           { "type": "string" },
                "state":          { "type": "string" }
            }
        }
    },

    "type": "object",
    "properties": {

        "billing_address": {
            "allOf": [
                { "$ref": "#/definitions/address" }
            ],
            "properties": {
                "street_address": {},
                "city": {},
                "state": {}                 
            },          
            "additionalProperties": false
            "required": ["street_address", "city", "state"] 
        },

        "shipping_address": {
            "allOf": [
                { "$ref": "#/definitions/address" },
                {
                    "properties": {
                        "type": {
                            "enum": ["residential","business"]
                        }
                    }
                }
            ],
            "properties": {
                "street_address": {},
                "city": {},
                "state": {},
                "type": {}                          
            },              
            "additionalProperties": false
            "required": ["street_address","city","state","type"] 
        }

    }
}

Each of your billing_address and shipping_address should specify their own required properties.

Your definition should not have "additionalProperties": false if you want to combine his properties with other ones.

Yves M.
  • 29,855
  • 23
  • 108
  • 144
  • 1
    If I'm reading this correctly, the definition of billing_address pulls in by reference the properties street_address, city and state; then it defines them again locally. Is that right? The shipping_address seems to do something similar. It appears redundant, and my goal of using $ref is to avoid repeating definitions. Pls tell me, what does this achieve? – chrisinmtown Jul 14 '15 at 15:07
  • You have to specify `street_address`, `city` and `state` again in `properties` in order to use them in `required`, that's why they are in `properties` with `{}` – Yves M. Jul 14 '15 at 15:38
  • "You have to specify street_address, city and state again in properties in order to use them in required, that's why they are in properties with {}". What no you don't. not according to the spec anyway. required, and properties are independent. – spinkus Feb 12 '16 at 05:50
  • 10
    This clearly violates [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), which is the reason why OP asked this question in the first place – LionC Nov 02 '16 at 12:22
  • I'd agree that this voilates DRY. Besides that it produces less-readable code and is in no way intuitive. I'm not sure if there's a reason why additionalProperties only looks at the sibling-level when checking allowed properties but IMHO this should be changed. – omni Feb 20 '18 at 10:47