Is there a way to validate that one of two properties (both arrays) are not empty?

4,997 views
Skip to first unread message

Kris Robison

unread,
Mar 19, 2015, 9:47:28 PM3/19/15
to json-...@googlegroups.com
Hi,

I've been digging through the specs for the last few days and I can't seem to find my question either in the docs or here or on StackOverflow.



Any of the following json objects are valid:

{ "id": 23, "foo": [1,2,3], "bar":[4,5,6] }

{ "id": 42, "foo": [1, 2, 3] } or { "id": 42, "foo": [1, 2, 3], "bar": [] }

{ "id": 42, "bar": [1] } or { "id": 42, "foo": [], "bar": [1] }

However the following are invalid:

{ "id": 11 }

{ "id": 11, "foo": [] } or  { "id": 12, "bar": [] }

{ "id": 2, "foo": [], "bar": [] }


I would like to set up a json-schema-4 document which catches the invalid cases. I know the following doesn't quite catch it.

{
  "type": "object",
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "#myObject",
"description": "The details of my object",
"additionalProperties": false,
"properties": {
"id": {
"type": "integer",
"description": "The ID of my object [Read Only Field]",
"minimum": 1
},
    "foo": {
"type": "array",
"items": {
"type": "integer"
},
"description": "List of foo ids",
      "minItems": 1
    },
"bar": {
"type": "array",
      "items": {
"type": "integer"
},
"description": "List of bar ids",
"minItems": 1
    }
  }
}

Is there some magical mixture of anyOf and oneOf that would help me get there? I only want one "foo" and one "bar" property in the resource.


Thanks!



Jason Desrosiers

unread,
Mar 20, 2015, 2:20:02 AM3/20/15
to json-...@googlegroups.com
It can be done, but it's not pretty.  The syntax for requiring property "foo" OR "bar" is not great in json-schema, but it really gets weird when you allow the arrays to be empty.

The object is valid if it matches one of two objects;
1) The object requires an "id" and a "bar" array with at least one item.  A "foo" array is optional and can be empty.
2) The object requires an "id" and a "foo" array with at least one item.  A "bar" array is optional and can be empty.

I used "definitions" to avoid duplicating the definitions of "id", "foo", and "bar" in the "anyOf" construct.

I used "allOf" to mixin the "minItems" constraint to the "foo" and "bar" definitions.
    "description": "The details of my object",
    "anyOf": [
        {
            "type": "object",
            "properties": {
                "id": {
                    "$ref": "#/definitions/id"
                },
                "foo": {
                    "$ref": "#/definitions/foo"
                },
                "bar": {
                    "allOf": [
                        {
                            "$ref": "#/definitions/bar"
                        },
                        {
                            "minItems": 1
                        }
                    ]
                }
            },
            "additionalProperties": false,
            "required": [
                "id",
                "bar"
            ]
        },
        {
            "type": "object",
            "properties": {
                "id": {
                    "$ref": "#/definitions/id"
                },
                "foo": {
                    "allOf": [
                        {
                            "$ref": "#/definitions/foo"
                        },
                        {
                            "minItems": 1
                        }
                    ]
                },
                "bar": {
                    "$ref": "#/definitions/bar"
                }
            },
            "additionalProperties": false,
            "required": [
                "id",
                "foo"
            ]
        }
    ],
    "definitions": {
        "id": {
            "type": "integer",
            "description": "The ID of my object [Read Only Field]",
            "minimum": 1
        },
        "foo": {
            "type": "array",
            "items": {
                "type": "integer"
            },
            "description": "List of foo ids"
        },

Francis Galiegue

unread,
Mar 20, 2015, 5:28:33 AM3/20/15
to json-...@googlegroups.com
On Fri, Mar 20, 2015 at 7:20 AM, Jason Desrosiers <jdes...@gmail.com> wrote:
> It can be done, but it's not pretty. The syntax for requiring property
> "foo" OR "bar" is not great in json-schema, but it really gets weird when
> you allow the arrays to be empty.
>

There is much easier than that...

{
"$schema": "http://json-schema/draftv4/schema#",
"type": "object",
"properties": {
"id": { "type": "integer" }
}
"additionalProperties": { "$ref": "#/definitions/nonEmptyArray" },
"oneOf": [
{
"required": [ "id", "foo" ],
"additionalProperties": false
},
{
"required": [ "id", "bar" ],
"additionalProperties": false
}
],
"definitions": {
"nonEmptyArray": {
"type": "array",
"minItems": 1,
"items": { /* schema for array items here */
}
}
}

--
Francis Galiegue, fgal...@gmail.com, https://github.com/fge
JSON Schema in Java: http://json-schema-validator.herokuapp.com
Parsers in pure Java: https://github.com/fge/grappa

Matt Jones

unread,
Mar 20, 2015, 12:20:08 PM3/20/15
to json-...@googlegroups.com


On Thursday, 19 March 2015 21:47:28 UTC-4, Kris Robison wrote:
Hi,

I've been digging through the specs for the last few days and I can't seem to find my question either in the docs or here or on StackOverflow.



Any of the following json objects are valid:

{ "id": 23, "foo": [1,2,3], "bar":[4,5,6] }

{ "id": 42, "foo": [1, 2, 3] } or { "id": 42, "foo": [1, 2, 3], "bar": [] }

{ "id": 42, "bar": [1] } or { "id": 42, "foo": [], "bar": [1] }

However the following are invalid:

{ "id": 11 }

{ "id": 11, "foo": [] } or  { "id": 12, "bar": [] }

{ "id": 2, "foo": [], "bar": [] }


I would like to set up a json-schema-4 document which catches the invalid cases. I know the following doesn't quite catch it.
 
Is there some magical mixture of anyOf and oneOf that would help me get there? I only want one "foo" and one "bar" property in the resource.

I originally misinterpreted your question slightly as wanting to ensure that ONLY one of foo or bar was nonempty. Here's a schema that will detect that:

{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "foo": { "type": "array" },
    "bar": { "type": "array" }
  },
  "additionalProperties": false,
  "oneOf": [
    {
      "properties": {
        "foo": { "minItems": 1 },
        "bar": { "maxItems": 0 }
      }
    },
    {
      "properties": {
        "foo": { "maxItems": 0 },
        "bar": { "minItems": 1 }
      }
    }
  ]
}

Note that your first example will NOT validate according to this, as either foo or bar MUST be empty/missing for this schema.

The schema you want is actually simpler:

{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "foo": { "type": "array" },
    "bar": { "type": "array" }
  },
  "additionalProperties": false,
  "not":{
    "properties": {
      "foo": { "maxItems": 0 },
      "bar": { "maxItems": 0 }
    }
  }
}

This one is just asserting that not both of the arrays are empty/missing; !(x & y) then simplifies to !x | !y, which is what was originally described.

Tested with the Ruby json_schema gem (v0.4.0) if it matters - I was slightly surprised that maxItems worked against an array that wasn't even in the payload...

--Matt Jones

Kris Robison

unread,
Mar 20, 2015, 12:56:50 PM3/20/15
to json-...@googlegroups.com
Thank you very much for looking at this. Testing it in the json_schema gem is perfect. I'm trying to set up a pacto server to look at our endpoints.

Kris Robison

unread,
Mar 20, 2015, 1:54:12 PM3/20/15
to json-...@googlegroups.com
Just an FYI Matt, the above schema allowed the {"id": 11 } scenario to successfully pass with the json-schema-v2.5.1 (note the hyphen) gem which is what pacto uses instead of json_schema.  Now I need to figure out which interpretation is correct.

Jason Desrosiers

unread,
Mar 20, 2015, 1:55:26 PM3/20/15
to json-...@googlegroups.com
Matt,

Very clever use of "not" and "maxItems".  I too was surprised at first that "maxItems" worked against an array that wasn't even in the payload.  After thinking about it, it makes sense.  It's actually not validating against the missing array at all.  Because neither of these arrays are required properties, simply not being in the payload is enough to make it valid.  It never has to check the number of items in the array because it has already been determined to be valid because it is missing.


On Friday, March 20, 2015 at 9:20:08 AM UTC-7, Matt Jones wrote:

Kris Robison

unread,
Mar 20, 2015, 2:19:01 PM3/20/15
to json-...@googlegroups.com
Actually both json_schema and json-schema pass the scenario: { "id": 11 }. At least one of the attributes is required and should be non-empty. I'm trying to add an anyOf clause now.

Kris Robison

unread,
Mar 20, 2015, 2:30:37 PM3/20/15
to json-...@googlegroups.com
I tried adding the following after the not definition, but it didn't help:

"anyOf": [
    {"foo": { "type": "array", "minItems": 1} },
    {"bar": { "type": "array", "minItems": 1} }
]

Jason Desrosiers

unread,
Mar 20, 2015, 2:55:02 PM3/20/15
to json-...@googlegroups.com
I think both validators are wrong for the reason I just described.  If both "foo" and "bar" are not present, the "not" clause should be valid which should make the overall validation fail.  Here are a couple validators that agree with me.


Anyway, you should be able to do this with "anyOf" instead of "not" like this:
{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "foo": { "type": "array" },
    "bar": { "type": "array" }
  },
  "additionalProperties": false,
  "anyOf": [
    {
      "properties": {
        "foo": { "minItems": 1 }
      },
      "required": [ "foo" ]
    },
    {
      "properties": {
        "bar": { "minItems": 1 }
      },
      "required": [ "bar" ]
    }
  ]
}

But, if we are playing golf (fewest loc wins), I would try setting "minProperties" to two and make "id" required.  This will ensure "foo" or "bar" is always present.
{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "foo": { "type": "array" },
    "bar": { "type": "array" }
  },
  "additionalProperties": false,
  "not": {
    "properties": {
      "foo": { "maxItems": 0 },
      "bar": { "maxItems": 0 }
    }
  },
  "minProperties": 2,
  "required": ["id"]
}

Kris Robison

unread,
Mar 20, 2015, 4:58:54 PM3/20/15
to json-...@googlegroups.com
Thanks Jason. Your version works with the libraries, although I'm going to open tickets on them. I like the creativity of your golf solution, but of course my real object has more optional properties, so that would not work.
Reply all
Reply to author
Forward
0 new messages