"""Unit tests for src/utils/schema_conversion.py.""" import json import re from typing import Any import pytest from pydantic import BaseModel, ValidationError from src.utils.schema_conversion import json_response_schema_to_pydantic def _object(properties: dict[str, Any], **extra: Any) -> dict[str, Any]: return {"type": "object", "properties": properties, **extra} class TestPrimitives: def test_flat_object_with_primitives(self): model = json_response_schema_to_pydantic( _object( { "name": {"type": "string"}, "age": {"type": "integer"}, "score": {"type": "number"}, "active": {"type": "boolean"}, }, required=["name", "age"], ) ) instance = model.model_validate( {"name": "ada", "age": 36, "score": 9.5, "active": True} ) assert instance.name == "ada" # pyright: ignore assert instance.age == 36 # pyright: ignore def test_required_field_missing_fails(self): model = json_response_schema_to_pydantic( _object({"name": {"type": "string"}}, required=["name"]) ) with pytest.raises(ValidationError): model.model_validate({}) def test_optional_field_defaults_to_none(self): model = json_response_schema_to_pydantic( _object({"nickname": {"type": "string"}}) ) instance = model.model_validate({}) assert instance.nickname is None # pyright: ignore def test_default_value(self): model = json_response_schema_to_pydantic( _object({"count": {"type": "integer", "default": 3}}) ) assert model.model_validate({}).count == 3 # pyright: ignore def test_null_type(self): model = json_response_schema_to_pydantic( _object({"nothing": {"type": "null"}}, required=["nothing"]) ) assert model.model_validate({"nothing": None}).nothing is None # pyright: ignore def test_default_wins_over_required(self): model = json_response_schema_to_pydantic( _object({"count": {"type": "integer", "default": 3}}, required=["count"]) ) assert model.model_validate({}).count == 3 # pyright: ignore class TestNesting: def test_nested_object(self): model = json_response_schema_to_pydantic( _object( { "address": _object( { "city": {"type": "string"}, "geo": _object( {"lat": {"type": "number"}}, required=["lat"] ), }, required=["city", "geo"], ) }, required=["address"], ) ) instance = model.model_validate( {"address": {"city": "oakland", "geo": {"lat": 37.8}}} ) assert instance.address.geo.lat == 37.8 # pyright: ignore def test_array_of_objects(self): model = json_response_schema_to_pydantic( _object( { "items": { "type": "array", "items": _object( {"food": {"type": "string"}}, required=["food"] ), } }, required=["items"], ) ) instance = model.model_validate({"items": [{"food": "sushi"}]}) assert instance.items[0].food == "sushi" # pyright: ignore def test_array_without_items_accepts_anything(self): model = json_response_schema_to_pydantic( _object({"stuff": {"type": "array"}}, required=["stuff"]) ) instance = model.model_validate({"stuff": [1, "two", {"three": 3}]}) assert len(instance.stuff) == 3 # pyright: ignore def test_nested_model_name_collision(self): # Two sibling objects whose name hints collide must not clash. model = json_response_schema_to_pydantic( _object( { "a": _object({"x b": _object({"v": {"type": "string"}})}), "a_x": _object({"b": _object({"v": {"type": "integer"}})}), } ) ) instance = model.model_validate( {"a": {"x b": {"v": "s"}}, "a_x": {"b": {"v": 1}}} ) assert instance.a_x.b.v == 1 # pyright: ignore class TestEnumsAndUnions: def test_string_enum(self): model = json_response_schema_to_pydantic( _object( {"sentiment": {"enum": ["loves", "hates"]}}, required=["sentiment"], ) ) assert model.model_validate({"sentiment": "loves"}).sentiment == "loves" # pyright: ignore with pytest.raises(ValidationError): model.model_validate({"sentiment": "meh"}) def test_int_enum_and_null_member(self): model = json_response_schema_to_pydantic( _object({"level": {"enum": [1, 2, None]}}, required=["level"]) ) assert model.model_validate({"level": None}).level is None # pyright: ignore assert model.model_validate({"level": 2}).level == 2 # pyright: ignore def test_invalid_enum_value_type(self): with pytest.raises(ValueError, match="enum values"): json_response_schema_to_pydantic(_object({"bad": {"enum": [[1]]}})) def test_anyof_with_null_is_optional(self): model = json_response_schema_to_pydantic( _object( {"maybe": {"anyOf": [{"type": "string"}, {"type": "null"}]}}, required=["maybe"], ) ) assert model.model_validate({"maybe": None}).maybe is None # pyright: ignore assert model.model_validate({"maybe": "x"}).maybe == "x" # pyright: ignore def test_oneof_union(self): model = json_response_schema_to_pydantic( _object( {"value": {"oneOf": [{"type": "integer"}, {"type": "string"}]}}, required=["value"], ) ) assert model.model_validate({"value": 5}).value == 5 # pyright: ignore def test_type_list_form(self): model = json_response_schema_to_pydantic( _object({"name": {"type": ["string", "null"]}}, required=["name"]) ) assert model.model_validate({"name": None}).name is None # pyright: ignore def test_all_null_enum_degenerates_to_none(self): model = json_response_schema_to_pydantic( _object({"nothing": {"enum": [None]}}, required=["nothing"]) ) assert model.model_validate({"nothing": None}).nothing is None # pyright: ignore with pytest.raises(ValidationError): model.model_validate({"nothing": "x"}) def test_union_of_objects(self): model = json_response_schema_to_pydantic( _object( { "pet": { "anyOf": [ _object({"meows": {"type": "boolean"}}, required=["meows"]), _object({"barks": {"type": "boolean"}}, required=["barks"]), ] } }, required=["pet"], ) ) instance = model.model_validate({"pet": {"barks": True}}) assert instance.pet.barks is True # pyright: ignore class TestRejections: @pytest.mark.parametrize( "construct,schema", [ ("$defs", _object({"a": {"type": "object", "$defs": {}}})), ("definitions", _object({"a": {"type": "object", "definitions": {}}})), ("allOf", _object({"a": {"allOf": [{"type": "string"}]}})), ("not", _object({"a": {"not": {"type": "string"}}})), ("if", _object({"a": {"if": {"type": "string"}}})), ( "patternProperties", _object({"a": {"type": "object", "patternProperties": {}}}), ), ], ) def test_unsupported_constructs(self, construct: str, schema: dict[str, Any]): with pytest.raises(ValueError, match=re.escape(construct)): json_response_schema_to_pydantic(schema) def test_error_message_includes_path(self): with pytest.raises( ValueError, match=r"unsupported \$ref '#/x'.*at properties\.address" ): json_response_schema_to_pydantic(_object({"address": {"$ref": "#/x"}})) def test_schema_valued_additional_properties(self): with pytest.raises(ValueError, match="additionalProperties"): json_response_schema_to_pydantic( _object( { "map": { "type": "object", "additionalProperties": {"type": "string"}, } } ) ) def test_boolean_schema(self): with pytest.raises(ValueError, match="boolean schemas"): json_response_schema_to_pydantic(_object({"anything": True})) def test_unknown_type(self): with pytest.raises(ValueError, match="unsupported type 'date'"): json_response_schema_to_pydantic(_object({"when": {"type": "date"}})) class TestRefs: def test_ref_into_defs(self): model = json_response_schema_to_pydantic( _object( {"address": {"$ref": "#/$defs/Address"}}, required=["address"], **{ "$defs": { "Address": _object( {"city": {"type": "string"}}, required=["city"] ) } }, ) ) instance = model.model_validate({"address": {"city": "Berlin"}}) assert instance.address.city == "Berlin" # pyright: ignore def test_ref_into_definitions_alias(self): model = json_response_schema_to_pydantic( _object( {"item": {"$ref": "#/definitions/Item"}}, required=["item"], definitions={"Item": {"type": "string"}}, ) ) assert model.model_validate({"item": "x"}).item == "x" # pyright: ignore def test_pydantic_nested_model_schema(self): """The real-world motivation: model_json_schema() of a nested Pydantic model emits $defs/$ref and must convert cleanly.""" class Preference(BaseModel): food: str confidence: float class Preferences(BaseModel): preferences: list[Preference] summary: str model = json_response_schema_to_pydantic(Preferences.model_json_schema()) instance = model.model_validate( { "preferences": [{"food": "sushi", "confidence": 0.9}], "summary": "likes sushi", } ) assert instance.preferences[0].food == "sushi" # pyright: ignore def test_root_ref(self): model = json_response_schema_to_pydantic( { "$ref": "#/$defs/Root", "$defs": { "Root": _object({"ok": {"type": "boolean"}}, required=["ok"]) }, } ) assert model.model_validate({"ok": True}).ok is True # pyright: ignore def test_ref_sibling_keys_overlay_target(self): model = json_response_schema_to_pydantic( _object( {"count": {"$ref": "#/$defs/Count", "default": 3}}, **{"$defs": {"Count": {"type": "integer"}}}, ) ) assert model.model_validate({}).count == 3 # pyright: ignore def test_same_def_referenced_twice(self): model = json_response_schema_to_pydantic( _object( { "home": {"$ref": "#/$defs/Address"}, "work": {"$ref": "#/$defs/Address"}, }, required=["home", "work"], **{"$defs": {"Address": _object({"city": {"type": "string"}})}}, ) ) instance = model.model_validate( {"home": {"city": "Berlin"}, "work": {"city": "Kyiv"}} ) assert instance.work.city == "Kyiv" # pyright: ignore def test_chained_refs(self): model = json_response_schema_to_pydantic( _object( {"a": {"$ref": "#/$defs/A"}}, required=["a"], **{"$defs": {"A": {"$ref": "#/$defs/B"}, "B": {"type": "string"}}}, ) ) assert model.model_validate({"a": "x"}).a == "x" # pyright: ignore def test_unreferenced_invalid_def_is_ignored(self): model = json_response_schema_to_pydantic( _object( {"name": {"type": "string"}}, **{"$defs": {"Broken": {"allOf": [{"type": "string"}]}}}, ) ) assert model.model_validate({"name": "x"}).name == "x" # pyright: ignore @pytest.mark.parametrize( "ref", ["#", "#/x", "#/$defs/a/b", "#/properties/a", "https://x.dev/s.json#/$defs/X"], ) def test_unsupported_ref_forms(self, ref: str): with pytest.raises(ValueError, match=r"unsupported \$ref"): json_response_schema_to_pydantic( _object( {"a": {"$ref": ref}}, **{"$defs": {"a": {"type": "string"}}}, ) ) def test_unknown_definition(self): with pytest.raises(ValueError, match="unknown definition"): json_response_schema_to_pydantic( _object({"a": {"$ref": "#/$defs/Missing"}}, **{"$defs": {}}) ) def test_direct_recursion_rejected(self): with pytest.raises(ValueError, match=r"recursive \$ref.*cycle: Node -> Node"): json_response_schema_to_pydantic( _object( {"tree": {"$ref": "#/$defs/Node"}}, **{ "$defs": { "Node": _object( { "children": { "type": "array", "items": {"$ref": "#/$defs/Node"}, } } ) } }, ) ) def test_mutual_recursion_rejected(self): with pytest.raises(ValueError, match=r"cycle: A -> B -> A"): json_response_schema_to_pydantic( _object( {"a": {"$ref": "#/$defs/A"}}, **{ "$defs": { "A": _object({"b": {"$ref": "#/$defs/B"}}), "B": _object({"a": {"$ref": "#/$defs/A"}}), } }, ) ) def test_recursive_pydantic_model_rejected(self): class Node(BaseModel): value: str children: list["Node"] = [] with pytest.raises(ValueError, match=r"recursive \$ref"): json_response_schema_to_pydantic(Node.model_json_schema()) def test_ref_expansion_counts_against_node_budget(self): """A doubling ref chain (billion laughs) is stopped by max_nodes.""" defs = { f"L{i}": _object( { "a": {"$ref": f"#/$defs/L{i + 1}"}, "b": {"$ref": f"#/$defs/L{i + 1}"}, } ) for i in range(10) } defs["L10"] = {"type": "string"} with pytest.raises(ValueError, match="maximum of .* nodes"): json_response_schema_to_pydantic( _object({"root": {"$ref": "#/$defs/L0"}}, **{"$defs": defs}) ) def test_duplicate_name_across_defs_and_definitions(self): with pytest.raises(ValueError, match="appears in both"): json_response_schema_to_pydantic( _object( {"a": {"$ref": "#/$defs/X"}}, **{ "$defs": {"X": {"type": "string"}}, "definitions": {"X": {"type": "integer"}}, }, ) ) def test_root_must_be_object(self): with pytest.raises(ValueError, match="root schema"): json_response_schema_to_pydantic({"type": "string"}) def test_root_must_be_dict(self): with pytest.raises(ValueError, match="JSON Schema object"): json_response_schema_to_pydantic(["not", "a", "schema"]) # pyright: ignore def test_no_recognizable_type(self): with pytest.raises(ValueError, match="no recognizable type"): json_response_schema_to_pydantic(_object({"mystery": {}})) def test_depth_limit(self): schema: dict[str, Any] = {"type": "string"} for _ in range(25): schema = _object({"inner": schema}) with pytest.raises(ValueError, match="maximum depth"): json_response_schema_to_pydantic(schema) def test_node_limit(self): schema = _object({f"field_{i}": {"type": "string"} for i in range(600)}) with pytest.raises(ValueError, match="maximum of 500 nodes"): json_response_schema_to_pydantic(schema) def test_property_schema_not_an_object(self): with pytest.raises(ValueError, match="schema must be an object"): json_response_schema_to_pydantic(_object({"a": "string"})) @pytest.mark.parametrize("members", [[], "not-a-list"]) def test_malformed_anyof(self, members: Any): with pytest.raises(ValueError, match="'anyOf' must be a non-empty array"): json_response_schema_to_pydantic(_object({"a": {"anyOf": members}})) def test_empty_type_list(self): with pytest.raises(ValueError, match="'type' array must not be empty"): json_response_schema_to_pydantic(_object({"a": {"type": []}})) @pytest.mark.parametrize("values", [[], "loves"]) def test_malformed_enum(self, values: Any): with pytest.raises(ValueError, match="'enum' must be a non-empty array"): json_response_schema_to_pydantic(_object({"a": {"enum": values}})) def test_properties_not_an_object(self): with pytest.raises(ValueError, match="'properties' must be an object"): json_response_schema_to_pydantic({"type": "object", "properties": []}) @pytest.mark.parametrize("required", ["a", [1]]) def test_malformed_required(self, required: Any): with pytest.raises(ValueError, match="'required' must be an array of strings"): json_response_schema_to_pydantic( _object({"a": {"type": "string"}}, required=required) ) def test_empty_property_name(self): with pytest.raises(ValueError, match="property names"): json_response_schema_to_pydantic(_object({"": {"type": "string"}})) class TestLenientAcceptance: def test_additional_properties_false_ignored(self): model = json_response_schema_to_pydantic( _object( {"known": {"type": "string"}}, required=["known"], additionalProperties=False, ) ) instance = model.model_validate({"known": "x", "extra": "dropped"}) assert instance.model_dump() == {"known": "x"} def test_root_dollar_schema_ignored(self): model = json_response_schema_to_pydantic( { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": {"a": {"type": "string"}}, } ) assert issubclass(model, BaseModel) def test_empty_properties(self): model = json_response_schema_to_pydantic({"type": "object", "properties": {}}) assert model.model_validate({}).model_dump() == {} def test_missing_properties_with_object_type(self): model = json_response_schema_to_pydantic({"type": "object"}) assert model.model_validate({"anything": 1}).model_dump() == {} def test_missing_type_with_properties_treated_as_object(self): model = json_response_schema_to_pydantic( {"properties": {"a": {"type": "string"}}, "required": ["a"]} ) assert model.model_validate({"a": "x"}).a == "x" # pyright: ignore def test_required_naming_unknown_property_ignored(self): model = json_response_schema_to_pydantic( _object({"a": {"type": "string"}}, required=["a", "ghost"]) ) assert model.model_validate({"a": "x"}).a == "x" # pyright: ignore class TestFieldMetadata: def test_description_propagates(self): model = json_response_schema_to_pydantic( _object({"food": {"type": "string", "description": "A food item"}}) ) generated = model.model_json_schema() assert generated["properties"]["food"]["description"] == "A food item" def test_constraint_hints_pass_through_unenforced(self): model = json_response_schema_to_pydantic( _object( { "tags": { "type": "array", "items": {"type": "string"}, "maxItems": 3, } }, required=["tags"], ) ) generated = model.model_json_schema() assert generated["properties"]["tags"]["maxItems"] == 3 # Not enforced: more than maxItems still validates. instance = model.model_validate({"tags": ["a", "b", "c", "d"]}) assert len(instance.tags) == 4 # pyright: ignore def test_non_identifier_key_alias_round_trip(self): model = json_response_schema_to_pydantic( _object( {"my-key": {"type": "string"}, "_private": {"type": "integer"}}, required=["my-key"], ) ) instance = model.model_validate({"my-key": "v", "_private": 7}) dumped = instance.model_dump_json(by_alias=True) assert '"my-key":"v"' in dumped assert '"_private":7' in dumped def test_digit_leading_key_gets_field_prefix(self): model = json_response_schema_to_pydantic( _object({"123": {"type": "integer"}}, required=["123"]) ) instance = model.model_validate({"123": 7}) assert instance.model_dump(by_alias=True) == {"123": 7} def test_sanitized_key_collision_round_trip(self): # "my-key" sanitizes to "my_key", which then collides with the real # "my_key" property; both must survive with their original JSON keys. model = json_response_schema_to_pydantic( _object( {"my-key": {"type": "string"}, "my_key": {"type": "integer"}}, required=["my-key", "my_key"], ) ) instance = model.model_validate({"my-key": "v", "my_key": 7}) assert instance.model_dump(by_alias=True) == {"my-key": "v", "my_key": 7} def test_digit_leading_model_name(self): model = json_response_schema_to_pydantic( _object({"a": {"type": "string"}}), model_name="123" ) assert model.__name__ == "Model123" class TestZodCompatibility: def test_zod4_tojsonschema_output_converts(self): # Captured shape of zod 4's z.toJSONSchema() for # z.object({ preferences: z.array(z.object({ food: z.string(), # sentiment: z.enum(["loves","hates"]) })), summary: z.string(), # note: z.string().optional() }) schema = { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "preferences": { "type": "array", "items": { "type": "object", "properties": { "food": {"type": "string"}, "sentiment": { "type": "string", "enum": ["loves", "hates"], }, }, "required": ["food", "sentiment"], "additionalProperties": False, }, }, "summary": {"type": "string"}, "note": {"type": "string"}, }, "required": ["preferences", "summary"], "additionalProperties": False, } model = json_response_schema_to_pydantic(schema) instance = model.model_validate( { "preferences": [{"food": "sushi", "sentiment": "loves"}], "summary": "likes sushi", } ) assert instance.preferences[0].sentiment == "loves" # pyright: ignore assert instance.note is None # pyright: ignore class TestCustomGuardLimits: def test_custom_max_depth(self): schema: dict[str, Any] = {"type": "string"} for _ in range(5): schema = _object({"inner": schema}) with pytest.raises(ValueError, match="maximum depth of 3"): json_response_schema_to_pydantic(schema, max_depth=3) def test_custom_max_nodes(self): schema = _object({f"f{i}": {"type": "string"} for i in range(20)}) with pytest.raises(ValueError, match="maximum of 10 nodes"): json_response_schema_to_pydantic(schema, max_nodes=10) def test_depth_exactly_at_limit_allowed(self): # Leaf sits at depth == max_depth; only depth > max_depth must fail. schema: dict[str, Any] = {"type": "string"} for _ in range(3): schema = _object({"inner": schema}) model = json_response_schema_to_pydantic(schema, max_depth=3) assert issubclass(model, BaseModel) # The wiki spec's own request example (dialectic-enhancements ยง3.A.1). SPEC_EXAMPLE_SCHEMA = _object( { "preferences": { "type": "array", "items": _object( { "food": {"type": "string"}, "sentiment": { "type": "string", "enum": ["loves", "likes", "neutral", "dislikes", "hates"], }, "confidence": {"type": "number", "minimum": 0, "maximum": 1}, }, required=["food", "sentiment"], ), "maxItems": 3, }, "summary": {"type": "string"}, }, required=["preferences", "summary"], ) class TestEndToEnd: """Table tests running the full pipeline the server runs: convert the caller's schema, validate a payload against the generated model, and serialize it back with model_dump_json(by_alias=True).""" @pytest.mark.parametrize( "schema,payload,expected", [ pytest.param( SPEC_EXAMPLE_SCHEMA, { "preferences": [ { "food": "dark roast coffee", "sentiment": "loves", "confidence": 0.95, }, {"food": "sushi", "sentiment": "likes"}, ], "summary": "Coffee enthusiast.", }, { "preferences": [ { "food": "dark roast coffee", "sentiment": "loves", "confidence": 0.95, }, {"food": "sushi", "sentiment": "likes", "confidence": None}, ], "summary": "Coffee enthusiast.", }, id="spec-example", ), pytest.param( _object( { "user": _object( { "name": {"type": "string"}, "location": _object( { "lat": {"type": "number"}, "lon": {"type": "number"}, }, required=["lat", "lon"], ), }, required=["name", "location"], ) }, required=["user"], ), {"user": {"name": "ada", "location": {"lat": 37.8, "lon": -122.3}}}, {"user": {"name": "ada", "location": {"lat": 37.8, "lon": -122.3}}}, id="nested-three-levels", ), pytest.param( _object( {"my-key": {"type": "string"}, "first name": {"type": "string"}}, required=["my-key"], ), {"my-key": "v", "first name": "Ada"}, {"my-key": "v", "first name": "Ada"}, id="alias-keys-round-trip", ), pytest.param( _object( { "count": {"type": "integer", "default": 3}, "tag": {"type": "string", "default": "none"}, } ), {}, {"count": 3, "tag": "none"}, id="defaults-fill-omitted-fields", ), pytest.param( _object( { "a": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "b": {"type": ["integer", "null"]}, }, required=["a", "b"], ), {"a": None, "b": 2}, {"a": None, "b": 2}, id="nullable-via-anyof-and-type-list", ), pytest.param( _object( {"value": {"oneOf": [{"type": "integer"}, {"type": "string"}]}}, required=["value"], ), {"value": "five"}, {"value": "five"}, id="oneof-union-string-member", ), pytest.param( _object({"level": {"enum": [1, 2, None]}}, required=["level"]), {"level": None}, {"level": None}, id="enum-with-null-member", ), pytest.param( _object({"stuff": {"type": "array"}}, required=["stuff"]), {"stuff": [1, "two", {"three": 3}, None]}, {"stuff": [1, "two", {"three": 3}, None]}, id="array-without-items-accepts-anything", ), pytest.param( _object( { "tags": { "type": "array", "items": {"type": "string"}, "maxItems": 2, } }, required=["tags"], ), {"tags": ["a", "b", "c", "d"]}, {"tags": ["a", "b", "c", "d"]}, id="constraint-hints-not-enforced", ), pytest.param( _object({"known": {"type": "string"}}, required=["known"]), {"known": "x", "hallucinated": "dropped"}, {"known": "x"}, id="extra-keys-dropped", ), pytest.param( {"type": "object", "properties": {}}, {}, {}, id="empty-object", ), ], ) def test_construct_validate_serialize( self, schema: dict[str, Any], payload: dict[str, Any], expected: dict[str, Any], ): model = json_response_schema_to_pydantic(schema) instance = model.model_validate(payload) # by_alias=True mirrors DialecticAgent.answer's serialization. assert json.loads(instance.model_dump_json(by_alias=True)) == expected @pytest.mark.parametrize( "schema,payload", [ pytest.param( SPEC_EXAMPLE_SCHEMA, {"preferences": [{"food": "sushi"}], "summary": "s"}, id="missing-required-in-array-item", ), pytest.param( SPEC_EXAMPLE_SCHEMA, { "preferences": [{"food": "sushi", "sentiment": "adores"}], "summary": "s", }, id="invalid-enum-value", ), pytest.param( SPEC_EXAMPLE_SCHEMA, {"preferences": [{"food": "sushi", "sentiment": "likes"}]}, id="missing-required-top-level", ), pytest.param( _object( {"user": _object({"name": {"type": "string"}}, required=["name"])}, required=["user"], ), {"user": {}}, id="missing-required-nested", ), pytest.param( _object({"a": {"type": "string"}}, required=["a"]), {"a": None}, id="null-for-non-nullable", ), pytest.param( _object({"n": {"type": "integer"}}, required=["n"]), {"n": {"nested": "dict"}}, id="wrong-type-for-integer", ), ], ) def test_rejects_nonconforming_payloads( self, schema: dict[str, Any], payload: dict[str, Any] ): model = json_response_schema_to_pydantic(schema) with pytest.raises(ValidationError): model.model_validate(payload)