Search code examples
pythonjsonschemadefault-valuejson-schema-validatorpython-jsonschema

How do I require one of two jsonschema properties, but also set a default?


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?


Solution

  • 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.