6

I'm using a slightly modified version of the code from the JSON Schema FAQ to create a validator that sets default values:

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},
    )
DefaultValidatingDraft4Validator = extend_with_default(Draft4Validator)

And I have a JSON Schema like so:

{'definitions': {
  'obj': {'additionalProperties': False,
          'properties': {
            'foo': {'default': None, 'oneOf': [{'type': 'null'}, {'type': 'string'}]},
            'bar': {'default': None, 'oneOf': [{'type': 'null'}, {'type': 'string'}]},
            'baz': {'default': None, 'oneOf': [{'type': 'null'}, {'type': 'string'}]},
            'children': {'default': None, 'oneOf': [
              {'type': 'null'}, 
              {
                'items': {'$ref': '#/definitions/obj'},
                'minItems': 1, 
                'type': 'array'
              }
            ]}},
  'required': ['foo', 'bar', 'baz'],
  'type': 'object'}},
  'oneOf': [
    {'$ref': '#/definitions/obj'},
    {
      'items': {'$ref': '#/definitions/obj'},
      'minItems': 1, 
      'type': 'array'
    }
  ]
}

So basically, there's an object that can have foo/bar/baz fields, and the entire instance can either be one of those objects or a list of them. Additionally, each object can have a list of child objects in the children field.

When I try to run this code against a single object, it works fine, but it fails when I have a list of objects:

In [22]: DefaultValidatingDraft4Validator(schema).validate({'foo': 'hi'})

In [23]: DefaultValidatingDraft4Validator(schema).validate([{'foo': 'hi'}, {'baz': 'bye'}])

...
AttributeError: 'list' object has no attribute 'setdefault'

With the "children" field, I need a way to handle lists at every level of the schema validation. Is there a way to do that properly?

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
shroud
  • 725
  • 8
  • 13

3 Answers3

3

In the validator, the list that is causing the exception, is a valid element.

Changes Needed:

So you need to exclude the list from consideration by changing:

if "default" in subschema:
    instance.setdefault(property_, subschema["default"])

to:

if "default" in subschema and not isinstance(instance, list):
    instance.setdefault(property_, subschema["default"])

This was all that was needed to get the two test cases to pass.

Code:

from jsonschema import Draft4Validator, 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 and not isinstance(instance, list):
                instance.setdefault(property_, subschema["default"])

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

    return validators.extend(
        validator_class, {"properties": set_defaults},
    )
FillDefaultValidatingDraft4Validator = extend_with_default(Draft4Validator)

Test Code:

test_schema = {
    'definitions': {
        'obj': {'additionalProperties': False,
                'properties': {
                    'foo': {'default': None, 'oneOf': [{'type': 'null'}, {'type': 'string'}]},
                    'bar': {'default': None, 'oneOf': [{'type': 'null'}, {'type': 'string'}]},
                    'baz': {'default': None, 'oneOf': [{'type': 'null'}, {'type': 'string'}]},
                    'children': {'default': None, 'oneOf': [
                        {'type': 'null'},
                        {
                            'items': {'$ref': '#/definitions/obj'},
                            'minItems': 1,
                            'type': 'array'
                        }
                    ]}
                },
                'required': ['foo', 'bar', 'baz'],
                'type': 'object'}
    },
    'oneOf': [
        {'$ref': '#/definitions/obj'},
        {
          'items': {'$ref': '#/definitions/obj'},
          'minItems': 1,
          'type': 'array'
        }
    ]
}

for test_data in ({'foo': 'hi'}, [{'foo': 'hi'}, {'baz': 'bye'}], 
                  [{'children': [{'foo': 'hi'}, {'baz': 'bye'}]}]):
    FillDefaultValidatingDraft4Validator(test_schema).validate(test_data)
    print(test_data)

Results:

{'foo': 'hi', 'bar': None, 'baz': None, 'children': None}
[
    {'foo': 'hi', 'bar': None, 'baz': None, 'children': None}, 
    {'baz': 'bye', 'foo': None, 'bar': None, 'children': None}
]
[
    {'children': [
        {'foo': 'hi', 'bar': None, 'baz': None, 'children': None}, 
        {'baz': 'bye', 'foo': None, 'bar': None, 'children': None}
    ], 'foo': None, 'bar': None, 'baz': None}
]
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
0

Also you want to check if it is a valid schema to match to add the default, otherwise it'll add default defined in the unmatching schema. For example, if you're using the oneOf directive, you only want to add the default value from the matching schema in the oneOf list.

This code will do the work:

def extend_validator_with_default(validator_class: jsonschema.protocols.Validator):
    validate_properties = validator_class.VALIDATORS["properties"]

    def set_defaults(validator, properties, instance, schema):
        valid = True
        for error in validate_properties(
            validator,
            properties,
            instance,
            schema,
        ):
            valid = False
            yield error

        if valid:
            for property, subschema in properties.items():
                if "default" in subschema and not isinstance(instance, list):
                    instance.setdefault(property, subschema["default"])

    return jsonschema.validators.extend(validator_class, {"properties": set_defaults})
0

There are several missing features in the given solutions, e.g. the default values are not validated (so it is possible to set an invalid default value), the schema must contain empty objects as default values in nested schemas, etc. I created a new function for my own needs to handle these cases:

def extend_validator_with_default(validator_class):
    """Extend a validator to automatically set default values during validation."""
    _NO_DEFAULT = object()
    validate_properties = validator_class.VALIDATORS["properties"]

    def set_defaults_and_validate(validator, properties, instance, schema):
        drop_if_empty = set()
        new_instance = deepcopy(instance)
        for prop, subschema in properties.items():
            if prop in new_instance:
                continue
            obj_type = subschema.get("type", "")
            default_value = subschema.get("default", _NO_DEFAULT)
            if default_value is not _NO_DEFAULT:
                new_instance.setdefault(prop, default_value)
            elif obj_type == "object":
                new_instance.setdefault(prop, {})
                drop_if_empty.add(prop)

        is_valid = True
        for error in validate_properties(
            validator,
            properties,
            new_instance,
            schema,
        ):
            is_valid = False
            yield error

        for prop in drop_if_empty:
            instance_prop = new_instance[prop]
            if isinstance(instance_prop, Mapping) and len(instance_prop) == 0:
                del new_instance[prop]

        if is_valid:
            instance.update(new_instance)

    return validators.extend(
        validator_class,
        {"properties": set_defaults_and_validate},
    )

You can find this function and some tests here: https://gist.github.com/adrien-berchet/4da364bee20b9d4286f3e38161d4eb72

StormRider
  • 402
  • 1
  • 5
  • 13