0

I try to validate a jsonschema that defines a circle either with a radius or a diameter or neither and then just sets a default radius. This is my schema:

{
  "properties": {
    "position": {},
    "radius": {
      { "type": "number" }
    },
    "diameter": {
      { "type": "number" }
    }
  },
  "oneOf": [
    {
      "required": ["radius"]
    },
    {
      "required": ["diameter"]
    },
    {
      "properties": {
        "radius": {
          "default": 16
        }
      }
    }
  ],
  "additionalProperties": false
}

This is the validator, that sets the default value (as described at JSON schema FAQ):

from jsonschema import Draft7Validator, validators

def extend_with_default(validator_class):
  validate_properties = validator_class.VALIDATORS['properties']

  def set_defaults(validator, properties, instance, schema):
    for property, subschema in properties.items():
      if 'default' in subschema:
        instance.setdefault(property, subschema['default'])

    for error in validate_properties(
      validator, properties, instance, schema,
    ):
      yield error

  return validators.extend(
    validator_class, {'properties' : set_defaults},
  )
Validator = extend_with_default(Draft7Validator)

This validator sets the default value before I validate the schema, so I can only set the radius or neither, but setting the diameter will always raise an error. If I change this to validate first and set default values later (which I would rather not, but ok), then it sets the default radius despite the required diameter already existing.

Is there some way to implement this without hard-coding it by setting the default-radius in python?

EzPizza
  • 979
  • 1
  • 13
  • 22

1 Answers1

0

The validator does not need to be changed. This is a possible soluation:

{
  "properties": {
    "position": {},
    "radius": {
      { "type": "number" }
    },
    "diameter": {
      { "type": "number" }
    }
  },
  "if": {
    "not": {
      "anyOf": [
        {
          "required": ["radius"]
        },
        {
          "required": ["diameter"]
        }
      ]
    }
  },
  "then": {
    "properties": {
      "radius": {
        "default": 16
      }
    }
  },
  "oneOf": [
    {
      "required": ["radius"]
    },
    {
      "required": ["diameter"]
    }
  ],
  "additionalProperties": false
}

if will be only true, if neither radius or diameter are set. Only then will the default radius be set. Afterwards oneOf checks, if only one of the parameters is set at the same time.

EzPizza
  • 979
  • 1
  • 13
  • 22
  • The `oneOf` subschema is mutually exclusive with the `if` condition - it is impossible for one of radius and diameter to be present, while also neither of them being present. – Ether Feb 03 '21 at 18:04
  • That's exactly the trick! The if condition is only used to set the default values (if they don't already exist). After that, the validation can no longer reach the ```then``` part and only evaluate the ```oneOf``` subschema. – EzPizza Feb 03 '21 at 18:24
  • That's not how JSON Schema works. *All* the keywords are evaluated -- the oneOf subschemas will always be used. Perhaps you meant to put the oneOf inside an 'else' clause? – Ether Feb 04 '21 at 00:05
  • The evaluation happens in two steps here: `instance.setdefault()` will set every default value that it can reach. It does _not_ need to satisfy the schema to set the values, so `oneOf` is irrelevant here. `validate_properties()` will then always satisfy the schema. The if condition is (as you pointed out) also evaluated and equates to false, so `then` is never reached during validation. I tested it with all possible configurations of setting/omitting radius and diameter. – EzPizza Feb 04 '21 at 13:08
  • ok, that is very weird and contrary to the JSON Schema spec, but you've got to work with what the library gives you... – Ether Feb 04 '21 at 18:34
  • As you suggested, moving `oneOf` in an 'else' clause should also work and might be easier to read. – EzPizza Feb 05 '21 at 15:39