44from typing import TYPE_CHECKING
55from typing import Any
66from typing import Iterator
7+ from typing import Mapping
78from typing import Optional
89
910from jsonschema .exceptions import FormatError
1011from jsonschema .protocols import Validator
1112from jsonschema_path import SchemaPath
1213
14+ from openapi_core .validation .schemas .datatypes import (
15+ _EMPTY_STATE_TUPLE as _EMPTY_STATES_TUPLE ,
16+ )
17+ from openapi_core .validation .schemas .datatypes import (
18+ _EMPTY_STATES as _EMPTY_STATES_MAP ,
19+ )
1320from openapi_core .validation .schemas .datatypes import FormatValidator
1421from openapi_core .validation .schemas .datatypes import ValidationState
1522from openapi_core .validation .schemas .exceptions import InvalidSchemaValue
@@ -40,19 +47,88 @@ def validate(self, value: Any) -> None:
4047 schema_type = (self .schema / "type" ).read_str_or_list ("any" )
4148 raise InvalidSchemaValue (value , schema_type , schema_errors = errors )
4249
50+ # Cache the recursive "does this schema benefit from a ValidationState?"
51+ # check, keyed on the SchemaPath. SchemaPath is hashed by content, so
52+ # two SchemaPaths pointing at the same spec location share a cache
53+ # slot regardless of identity -- safe across GC, bounded by the number
54+ # of distinct schema shapes in the spec rather than by input volume.
55+ _needs_state_cache : dict [SchemaPath , bool ] = {}
56+
57+ @classmethod
58+ def _schema_needs_state (cls , schema : SchemaPath ) -> bool :
59+ """True if building a ValidationState for ``schema`` carries
60+ information the unmarshaller can reuse: either composition
61+ (oneOf/anyOf/allOf) on this node, or a descendant that does.
62+
63+ Cycle-safe: a False sentinel is stored before recursing, so a
64+ $ref loop terminates and the real answer overwrites the
65+ sentinel once the recursion completes.
66+ """
67+ cache = cls ._needs_state_cache
68+ cached = cache .get (schema )
69+ if cached is not None :
70+ return cached
71+ # Self-composition is the strongest signal; check it first to
72+ # short-circuit the cheap case.
73+ if "oneOf" in schema or "anyOf" in schema or "allOf" in schema :
74+ cache [schema ] = True
75+ return True
76+ # Seed the in-progress sentinel for cycle protection.
77+ cache [schema ] = False
78+ # Recurse into children. We only need to find one descendant
79+ # that needs state to flip our own answer.
80+ result = False
81+ if "properties" in schema :
82+ prop_iter = (schema / "properties" ).items ()
83+ for prop_name , prop_schema in prop_iter :
84+ if not isinstance (prop_name , str ):
85+ continue
86+ if cls ._schema_needs_state (prop_schema ):
87+ result = True
88+ break
89+ if not result and "additionalProperties" in schema :
90+ try :
91+ ap = schema / "additionalProperties"
92+ except Exception :
93+ ap = None
94+ if ap is not None and cls ._schema_needs_state (ap ):
95+ result = True
96+ if not result and "items" in schema :
97+ try :
98+ items = schema / "items"
99+ except Exception :
100+ items = None
101+ if items is not None and cls ._schema_needs_state (items ):
102+ result = True
103+ cache [schema ] = result
104+ return result
105+
43106 def validate_state (self , value : Any ) -> ValidationState :
44107 self .validate (value )
45108 return self ._build_trusted_state (value )
46109
47110 def _build_trusted_state (self , value : Any ) -> ValidationState :
48- primitive_type = self .get_primitive_type (value )
49- property_states = {}
50- additional_property_states = {}
51- item_states : tuple [ValidationState , ...] = ()
52- one_of_state = None
53- any_of_states : tuple [ValidationState , ...] = ()
54- all_of_states : tuple [ValidationState , ...] = ()
111+ """Build a ValidationState for ``value`` against ``self.schema``.
55112
113+ Pre-condition: ``value`` has already been validated against the
114+ schema (typically by an outer ``validate_state``). This method
115+ does NOT re-validate -- it only records the composition-branch
116+ decisions and recurses into children that themselves need
117+ state.
118+ """
119+ primitive_type = self .get_primitive_type (value )
120+ property_states : Mapping [str , ValidationState ] = _EMPTY_STATES_MAP
121+ additional_property_states : Mapping [str , ValidationState ] = (
122+ _EMPTY_STATES_MAP
123+ )
124+ item_states : tuple [ValidationState , ...] = _EMPTY_STATES_TUPLE
125+ one_of_state : Optional [ValidationState ] = None
126+ any_of_states : tuple [ValidationState , ...] = _EMPTY_STATES_TUPLE
127+ all_of_states : tuple [ValidationState , ...] = _EMPTY_STATES_TUPLE
128+
129+ # Composition keywords: always cache the branch selection,
130+ # because re-resolving it at unmarshal time is exactly the work
131+ # ValidationState exists to avoid.
56132 if "oneOf" in self .schema :
57133 one_of_schema = self .get_one_of_schema (value )
58134 if one_of_schema is not None :
@@ -76,22 +152,41 @@ def _build_trusted_state(self, value: Any) -> ValidationState:
76152 for all_of_schema in all_of_schemas
77153 )
78154
155+ # Children: recurse only into sub-trees that themselves contain
156+ # composition. Sub-trees without composition can be unmarshalled
157+ # via the bare-state fast path -- no cached state needed.
79158 if primitive_type == "object" and isinstance (value , dict ):
159+ new_props : dict [str , ValidationState ] = {}
80160 for prop_name , prop_schema in self ._get_input_properties (
81161 value
82162 ).items ():
83- property_states [prop_name ] = self .evolve (
163+ if not self ._schema_needs_state (prop_schema ):
164+ continue
165+ new_props [prop_name ] = self .evolve (
84166 prop_schema
85167 )._build_trusted_state (value [prop_name ])
168+ if new_props :
169+ property_states = new_props
170+
171+ new_addl : dict [str , ValidationState ] = {}
86172 for (
87173 prop_name ,
88174 additional_prop_schema ,
89175 ) in self ._get_input_additional_properties (value ).items ():
90- additional_property_states [prop_name ] = self .evolve (
176+ if not self ._schema_needs_state (additional_prop_schema ):
177+ continue
178+ new_addl [prop_name ] = self .evolve (
91179 additional_prop_schema
92180 )._build_trusted_state (value [prop_name ])
181+ if new_addl :
182+ additional_property_states = new_addl
93183 elif primitive_type == "array" and isinstance (value , list ):
94- item_states = tuple (self .iter_item_states (value ))
184+ # Skip per-item state when the item schema itself doesn't
185+ # need state -- the unmarshaller's bare-state fast path
186+ # handles each item.
187+ built = self ._build_item_states_if_needed (value )
188+ if built :
189+ item_states = built
95190
96191 return ValidationState (
97192 self .schema ,
@@ -105,6 +200,19 @@ def _build_trusted_state(self, value: Any) -> ValidationState:
105200 all_of_states = all_of_states ,
106201 )
107202
203+ def _build_item_states_if_needed (
204+ self , value : list [Any ]
205+ ) -> tuple [ValidationState , ...]:
206+ if "items" not in self .schema :
207+ return _EMPTY_STATES_TUPLE
208+ items_schema = self .schema / "items"
209+ if not self ._schema_needs_state (items_schema ):
210+ return _EMPTY_STATES_TUPLE
211+ item_validator = self .evolve (items_schema )
212+ return tuple (
213+ item_validator ._build_trusted_state (item ) for item in value
214+ )
215+
108216 def evolve (self , schema : SchemaPath ) -> "SchemaValidator" :
109217 cls = self .__class__
110218
0 commit comments