← All ivy-nodes
IVYXSTUDIO · IVY NODE
S

Schema Validator

ivy.node.schema-validator · v0.1.0

ivyx✓

Checks a payload against a JSON Schema subset and reports every problem it finds, not just the first. In report mode it returns valid and a list of problems so a flow can branch; in enforce mode a mismatch raises and the run stops there. Supports type, required, properties, items, enum, minimum and maximum, and ignores keywords outside that set rather than failing on them.

#validation#schema#gate

Inputs

FieldTypeDescription
payloadrequiredNot declaredThe value to check. Any JSON type.
schemarequiredobjectThe JSON Schema subset to check against.
modestringreport returns the problems; enforce raises on the first mismatch.

Outputs

FieldTypeDescription
validrequiredbooleanTrue when no problem was found.
problemsrequiredarrayOne sentence per problem, each naming the path.

Source

python

# Input preparation
inp = __ivy_ctx__["nodes"][__ivy_node_id__]["input"]
payload = inp["payload"]
schema = inp["schema"]
mode = inp.get("mode", "report")

# Compute
TYPES = {
    "object": dict,
    "array": list,
    "string": str,
    "number": (int, float),
    "integer": int,
    "boolean": bool,
}


def type_of(value):
    """JSON Schema's name for a Python value. Booleans are not integers here."""
    if isinstance(value, bool):
        return "boolean"
    if isinstance(value, dict):
        return "object"
    if isinstance(value, list):
        return "array"
    if isinstance(value, str):
        return "string"
    if isinstance(value, int):
        return "integer"
    if isinstance(value, float):
        return "number"
    if value is None:
        return "null"
    return "unknown"


def validate(payload, schema, path="$"):
    """
    Check a payload against the subset of JSON Schema this node supports:
    `type`, `required`, `properties`, `items`, `enum`, `minimum`, `maximum`.

    It reports EVERY problem rather than the first one. A gate that stops at
    the first error makes a caller fix one field, run again, and find the
    next — and the point of a gate is to say what is wrong in one pass.

    The subset is deliberate and the node says so rather than pretending to be
    a full validator: an unknown keyword is ignored, never silently treated as
    a failure.
    """
    problems = []
    if not isinstance(schema, dict):
        raise ValueError("schema must be an object")

    expected = schema.get("type")
    if expected is not None:
        wanted = expected if isinstance(expected, list) else [expected]
        actual = type_of(payload)
        # An integer satisfies `number`; the reverse is not true.
        widened = wanted + (["integer"] if "number" in wanted else [])
        if actual not in widened:
            problems.append(f"{path}: expected {'/'.join(wanted)}, got {actual}")
            return problems

    if isinstance(payload, dict):
        for name in schema.get("required", []) or []:
            if name not in payload:
                problems.append(f"{path}.{name}: required")
        for name, sub in (schema.get("properties") or {}).items():
            if name in payload:
                problems += validate(payload[name], sub, f"{path}.{name}")

    if isinstance(payload, list) and isinstance(schema.get("items"), dict):
        for index, item in enumerate(payload):
            problems += validate(item, schema["items"], f"{path}[{index}]")

    if "enum" in schema and payload not in schema["enum"]:
        problems.append(f"{path}: {payload!r} is not one of {schema['enum']}")
    if "minimum" in schema and isinstance(payload, (int, float)) and not isinstance(payload, bool):
        if payload < schema["minimum"]:
            problems.append(f"{path}: {payload} is below the minimum {schema['minimum']}")
    if "maximum" in schema and isinstance(payload, (int, float)) and not isinstance(payload, bool):
        if payload > schema["maximum"]:
            problems.append(f"{path}: {payload} is above the maximum {schema['maximum']}")
    return problems


if mode not in ("report", "enforce"):
    raise ValueError("mode must be report or enforce")

found = validate(payload, schema)
if found and mode == "enforce":
    raise ValueError("payload does not match the schema: " + "; ".join(found))

# Output collection (runner reads __ivy_ctx__)
out = __ivy_ctx__["nodes"][__ivy_node_id__]["output"]
out["valid"] = not found
out["problems"] = found

Tests

Requires: python:3.9

  • accepts-a-good-payload

    A payload matching the schema is valid with no problems.

  • reports-every-problem

    A missing field and an out-of-range number are both reported in one pass.

  • boolean-is-not-an-integer

    True does not satisfy an integer field, which Python's own isinstance would allow.

  • enforce-stops-the-run

    In enforce mode a mismatch raises instead of returning a verdict.