from typing import Union, Dict, List JsonTypes = Union[Dict, List, str, int, float, bool, None] class ValidationError(ValueError): pass def _fmt_path(path: List) -> str: return f'schema[{"][".join(map(repr, path))}]' def _resolve_ref_not_recursive(sub_schema: JsonTypes, schema: JsonTypes) -> JsonTypes: """In case the sub_schema is a dict and has direct "$ref" keys, it is replaced by a dict where the "$ref" key is replaced by the corresponding definition in schema. Nested $ref keys are not resolved. Recursive $ref keys are not resolved. :param sub_schema: JSON sub-schema where a "$ref" key is possible replaced. The value of "$ref" could be e.g. "#/definitions/position" :param schema: JSON root schema containing definitions for the keys. :raise ValidationError: In case a "$ref" could not be resolved. """ if not isinstance(sub_schema, dict) or '$ref' not in sub_schema: return sub_schema ref = sub_schema['$ref'] if not isinstance(ref, str): raise ValidationError(f'Type of reference {ref} is not string.') path = ref.split('/') if len(path) == 0 or path[0] != '#': raise ValidationError(f'Unsupported reference {ref}.') ref_schema = schema for p in path[1:]: if not isinstance(ref_schema, dict) or p not in ref_schema: raise ValidationError(f'Reference path {ref} not found in schema.') ref_schema = ref_schema[p] if not isinstance(ref_schema, dict): raise ValidationError(f'Reference path {ref} is no dict.') sub_schema = sub_schema.copy() del sub_schema['$ref'] resolved_schema = ref_schema.copy() resolved_schema.update(sub_schema) return resolved_schema def _resolve_ref(sub_schema: JsonTypes, schema: JsonTypes) -> JsonTypes: """Same as `_resolve_ref_not_recursive` but recursively resolves $ref keys. However, does not resolve nested $ref keys. """ resolved_schema = sub_schema while isinstance(resolved_schema, dict) and '$ref' in resolved_schema: resolved_schema = _resolve_ref_not_recursive(resolved_schema, schema) return resolved_schema def _order_json_keys_string(sub_value: JsonTypes, sub_schema: JsonTypes, schema: JsonTypes, path: List) -> str: if not isinstance(sub_value, str): raise ValidationError(f'Type of {_fmt_path(path)} needs to be string (Python str).') return sub_value def _order_json_keys_number(sub_value: JsonTypes, sub_schema: JsonTypes, schema: JsonTypes, path: List) -> Union[int, float]: if not isinstance(sub_value, (int, float)) or isinstance(sub_value, bool): raise ValidationError(f'Type of {_fmt_path(path)} needs to be number (Python int or float).') return sub_value def _order_json_keys_object(sub_value: JsonTypes, sub_schema: JsonTypes, schema: JsonTypes, path: List) -> Dict: if not isinstance(sub_value, dict): raise ValidationError(f'Type of {_fmt_path(path)} needs to be object (Python dict).') v = sub_value.copy() p = sub_schema.get('properties', {}) result = {} for key in p: if key in v: result[key] = _order_json_keys(v.pop(key), p[key], schema, path + [key]) else: if key in sub_schema.get('required', []): raise ValidationError(f'Required key "{key}" not present ({_fmt_path(path)}).') if len(v) > 0: if sub_schema.get('additionalProperties', True): # strictly speaking additionalProperties could be more complicated than boolean result.update(v) else: raise ValidationError(f'Keys not allowed in {_fmt_path(path)}: {", ".join(v)}') return result def _order_json_keys_array(sub_value: JsonTypes, sub_schema: JsonTypes, schema: JsonTypes, path: List) -> List: if not isinstance(sub_value, list): raise ValidationError(f'Type of {"".join(_fmt_path(path))} needs to be array (Python list).') s = sub_schema.get('items', True) return [_order_json_keys(v, s, schema, path + [i]) for i, v in enumerate(sub_value)] def _order_json_keys_boolean(sub_value: JsonTypes, sub_schema: JsonTypes, schema: JsonTypes, path: List) -> bool: if not isinstance(sub_value, bool): raise ValidationError(f'Type of {_fmt_path(path)} needs to be boolean (Python bool).') return sub_value def _order_json_keys_null(sub_value: JsonTypes, sub_schema: JsonTypes, schema: JsonTypes, path: List) -> None: if sub_value is not None: raise ValidationError(f'Type of {_fmt_path(path)} needs to be null (Python None).') return sub_value def _order_json_keys(sub_value: JsonTypes, sub_schema: JsonTypes, schema: JsonTypes, path: List) -> JsonTypes: if isinstance(sub_schema, bool): if sub_schema: return sub_value raise ValidationError(f'Value {sub_value} not allowed in {_fmt_path(path)}.') if isinstance(sub_schema, dict): sub_schema = _resolve_ref(sub_schema, schema) return { 'string': _order_json_keys_string, 'number': _order_json_keys_number, 'integer': _order_json_keys_number, 'object': _order_json_keys_object, 'array': _order_json_keys_array, 'boolean': _order_json_keys_boolean, 'null': _order_json_keys_null, }[sub_schema['type']](sub_value, sub_schema, schema, path) def order_json_keys(value: JsonTypes, schema: JsonTypes) -> JsonTypes: return _order_json_keys(value, schema, schema, [])