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,91 @@ 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 iff 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+ try :
83+ prop_iter = (schema / "properties" ).items ()
84+ except Exception :
85+ prop_iter = ()
86+ for prop_name , prop_schema in prop_iter :
87+ if not isinstance (prop_name , str ):
88+ continue
89+ if cls ._schema_needs_state (prop_schema ):
90+ result = True
91+ break
92+ if not result and "additionalProperties" in schema :
93+ try :
94+ ap = schema / "additionalProperties"
95+ except Exception :
96+ ap = None
97+ if ap is not None and cls ._schema_needs_state (ap ):
98+ result = True
99+ if not result and "items" in schema :
100+ try :
101+ items = schema / "items"
102+ except Exception :
103+ items = None
104+ if items is not None and cls ._schema_needs_state (items ):
105+ result = True
106+ cache [schema ] = result
107+ return result
108+
43109 def validate_state (self , value : Any ) -> ValidationState :
44110 self .validate (value )
45111 return self ._build_trusted_state (value )
46112
47113 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 , ...] = ()
114+ """Build a ValidationState for ``value`` against ``self.schema``.
55115
116+ Pre-condition: ``value`` has already been validated against the
117+ schema (typically by an outer ``validate_state``). This method
118+ does NOT re-validate -- it only records the composition-branch
119+ decisions and recurses into children that themselves need
120+ state.
121+ """
122+ primitive_type = self .get_primitive_type (value )
123+ property_states : Mapping [str , ValidationState ] = _EMPTY_STATES_MAP
124+ additional_property_states : Mapping [str , ValidationState ] = (
125+ _EMPTY_STATES_MAP
126+ )
127+ item_states : tuple [ValidationState , ...] = _EMPTY_STATES_TUPLE
128+ one_of_state : Optional [ValidationState ] = None
129+ any_of_states : tuple [ValidationState , ...] = _EMPTY_STATES_TUPLE
130+ all_of_states : tuple [ValidationState , ...] = _EMPTY_STATES_TUPLE
131+
132+ # Composition keywords: always cache the branch selection,
133+ # because re-resolving it at unmarshal time is exactly the work
134+ # ValidationState exists to avoid.
56135 if "oneOf" in self .schema :
57136 one_of_schema = self .get_one_of_schema (value )
58137 if one_of_schema is not None :
@@ -76,22 +155,41 @@ def _build_trusted_state(self, value: Any) -> ValidationState:
76155 for all_of_schema in all_of_schemas
77156 )
78157
158+ # Children: recurse only into sub-trees that themselves contain
159+ # composition. Sub-trees without composition can be unmarshalled
160+ # via the bare-state fast path -- no cached state needed.
79161 if primitive_type == "object" and isinstance (value , dict ):
162+ new_props : dict [str , ValidationState ] = {}
80163 for prop_name , prop_schema in self ._get_input_properties (
81164 value
82165 ).items ():
83- property_states [prop_name ] = self .evolve (
166+ if not self ._schema_needs_state (prop_schema ):
167+ continue
168+ new_props [prop_name ] = self .evolve (
84169 prop_schema
85170 )._build_trusted_state (value [prop_name ])
171+ if new_props :
172+ property_states = new_props
173+
174+ new_addl : dict [str , ValidationState ] = {}
86175 for (
87176 prop_name ,
88177 additional_prop_schema ,
89178 ) in self ._get_input_additional_properties (value ).items ():
90- additional_property_states [prop_name ] = self .evolve (
179+ if not self ._schema_needs_state (additional_prop_schema ):
180+ continue
181+ new_addl [prop_name ] = self .evolve (
91182 additional_prop_schema
92183 )._build_trusted_state (value [prop_name ])
184+ if new_addl :
185+ additional_property_states = new_addl
93186 elif primitive_type == "array" and isinstance (value , list ):
94- item_states = tuple (self .iter_item_states (value ))
187+ # Skip per-item state when the item schema itself doesn't
188+ # need state -- the unmarshaller's bare-state fast path
189+ # handles each item.
190+ built = self ._build_item_states_if_needed (value )
191+ if built :
192+ item_states = built
95193
96194 return ValidationState (
97195 self .schema ,
@@ -105,6 +203,19 @@ def _build_trusted_state(self, value: Any) -> ValidationState:
105203 all_of_states = all_of_states ,
106204 )
107205
206+ def _build_item_states_if_needed (
207+ self , value : list
208+ ) -> tuple [ValidationState , ...]:
209+ if "items" not in self .schema :
210+ return _EMPTY_STATES_TUPLE
211+ items_schema = self .schema / "items"
212+ if not self ._schema_needs_state (items_schema ):
213+ return _EMPTY_STATES_TUPLE
214+ item_validator = self .evolve (items_schema )
215+ return tuple (
216+ item_validator ._build_trusted_state (item ) for item in value
217+ )
218+
108219 def evolve (self , schema : SchemaPath ) -> "SchemaValidator" :
109220 cls = self .__class__
110221
0 commit comments