@@ -8,14 +8,15 @@ import (
88 "errors"
99 "fmt"
1010 "log/slog"
11+ "math"
1112 "os"
1213 "reflect"
1314 "regexp"
1415 "strconv"
1516 "sync"
1617
18+ "github.com/pb33f/libopenapi-validator/cache"
1719 "github.com/pb33f/libopenapi/datamodel/high/base"
18- "github.com/pb33f/libopenapi/utils"
1920 "github.com/santhosh-tekuri/jsonschema/v6"
2021 "go.yaml.in/yaml/v4"
2122 "golang.org/x/text/language"
@@ -123,49 +124,114 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
123124 }
124125
125126 var renderedSchema []byte
126-
127- // render the schema, to be used for validation, stop this from running concurrently, mutations are made to state
128- // and, it will cause async issues.
129- // Create isolated render context for this validation to prevent false positive cycle detection
130- // when multiple validations run concurrently.
131- // Use validation mode to force full inlining of discriminator refs - the JSON schema compiler
132- // needs a self-contained schema without unresolved $refs.
133- renderCtx := base .NewInlineRenderContextForValidation ()
134- s .lock .Lock ()
135- var e error
136- renderedSchema , e = schema .RenderInlineWithContext (renderCtx )
137- if e != nil {
138- // schema cannot be rendered, so it's not valid!
139- violation := & liberrors.SchemaValidationFailure {
140- Reason : e .Error (),
141- Location : "unavailable" ,
142- ReferenceSchema : string (renderedSchema ),
143- ReferenceObject : string (payload ),
127+ var renderedNode * yaml.Node
128+ var compiledSchema * jsonschema.Schema
129+
130+ // Check cache first — reuses existing SchemaCache (populated by NewValidationOptions).
131+ var cacheKey uint64
132+ canCache := s .options .SchemaCache != nil && schema .GoLow () != nil
133+ if canCache {
134+ // Include version in key so 3.0 (nullable) and 3.1 compile differently.
135+ cacheKey = schema .GoLow ().Hash () ^ uint64 (math .Float32bits (version ))
136+ if cached , ok := s .options .SchemaCache .Load (cacheKey ); ok &&
137+ cached != nil && cached .CompiledSchema != nil {
138+ renderedSchema = cached .RenderedInline
139+ renderedNode = cached .RenderedNode
140+ compiledSchema = cached .CompiledSchema
144141 }
145- validationErrors = append (validationErrors , & liberrors.ValidationError {
146- ValidationType : helpers .RequestBodyValidation ,
147- ValidationSubType : helpers .Schema ,
148- Message : "schema does not pass validation" ,
149- Reason : fmt .Sprintf ("The schema cannot be decoded: %s" , e .Error ()),
150- SpecLine : schema .GoLow ().GetRootNode ().Line ,
151- SpecCol : schema .GoLow ().GetRootNode ().Column ,
152- SchemaValidationErrors : []* liberrors.SchemaValidationFailure {violation },
153- HowToFix : liberrors .HowToFixInvalidSchema ,
154- Context : string (renderedSchema ),
155- })
142+ }
143+
144+ // Cache miss — render, convert to JSON, and compile.
145+ if compiledSchema == nil {
146+ renderCtx := base .NewInlineRenderContextForValidation ()
147+ s .lock .Lock ()
148+ nodeIface , renderErr := schema .MarshalYAMLInlineWithContext (renderCtx )
156149 s .lock .Unlock ()
157- return false , validationErrors
158150
159- }
160- s .lock .Unlock ()
151+ if renderErr != nil {
152+ violation := & liberrors.SchemaValidationFailure {
153+ Reason : renderErr .Error (),
154+ Location : "unavailable" ,
155+ ReferenceSchema : string (renderedSchema ),
156+ ReferenceObject : string (payload ),
157+ }
158+ validationErrors = append (validationErrors , & liberrors.ValidationError {
159+ ValidationType : helpers .RequestBodyValidation ,
160+ ValidationSubType : helpers .Schema ,
161+ Message : "schema does not pass validation" ,
162+ Reason : fmt .Sprintf ("The schema cannot be decoded: %s" , renderErr .Error ()),
163+ SpecLine : schema .GoLow ().GetRootNode ().Line ,
164+ SpecCol : schema .GoLow ().GetRootNode ().Column ,
165+ SchemaValidationErrors : []* liberrors.SchemaValidationFailure {violation },
166+ HowToFix : liberrors .HowToFixInvalidSchema ,
167+ Context : string (renderedSchema ),
168+ })
169+ return false , validationErrors
170+ }
171+
172+ // MarshalYAMLInlineWithContext returns *yaml.Node (from NodeBuilder.Render)
173+ renderedNode , _ = nodeIface .(* yaml.Node )
161174
162- jsonSchema , _ := utils .ConvertYAMLtoJSON (renderedSchema )
175+ // yaml.Node → map → JSON bytes (skips yaml.Marshal + yaml.Unmarshal round-trip)
176+ var jsonMap map [string ]interface {}
177+ if renderedNode != nil {
178+ _ = renderedNode .Decode (& jsonMap )
179+ }
180+ jsonSchema , _ := json .Marshal (jsonMap )
181+
182+ // YAML bytes generated once for error messages / context strings
183+ renderedSchema , _ = yaml .Marshal (renderedNode )
184+
185+ path := ""
186+ if schema .GoLow ().GetIndex () != nil {
187+ path = schema .GoLow ().GetIndex ().GetSpecAbsolutePath ()
188+ }
189+
190+ var compileErr error
191+ compiledSchema , compileErr = helpers .NewCompiledSchemaWithVersion (path , jsonSchema , s .options , version )
192+ if compileErr != nil {
193+ violation := & liberrors.SchemaValidationFailure {
194+ Reason : compileErr .Error (),
195+ Location : "schema compilation" ,
196+ ReferenceSchema : string (renderedSchema ),
197+ ReferenceObject : string (payload ),
198+ }
199+ line := 1
200+ col := 0
201+ if schema .GoLow ().Type .KeyNode != nil {
202+ line = schema .GoLow ().Type .KeyNode .Line
203+ col = schema .GoLow ().Type .KeyNode .Column
204+ }
205+ validationErrors = append (validationErrors , & liberrors.ValidationError {
206+ ValidationType : helpers .Schema ,
207+ ValidationSubType : helpers .Schema ,
208+ Message : "schema compilation failed" ,
209+ Reason : fmt .Sprintf ("Schema compilation failed: %s" , compileErr .Error ()),
210+ SpecLine : line ,
211+ SpecCol : col ,
212+ SchemaValidationErrors : []* liberrors.SchemaValidationFailure {violation },
213+ HowToFix : liberrors .HowToFixInvalidSchema ,
214+ Context : string (renderedSchema ),
215+ })
216+ return false , validationErrors
217+ }
218+
219+ // Store in cache for subsequent validations of the same schema.
220+ if canCache && compiledSchema != nil {
221+ s .options .SchemaCache .Store (cacheKey , & cache.SchemaCacheEntry {
222+ Schema : schema ,
223+ RenderedInline : renderedSchema ,
224+ ReferenceSchema : string (renderedSchema ),
225+ RenderedJSON : jsonSchema ,
226+ CompiledSchema : compiledSchema ,
227+ RenderedNode : renderedNode ,
228+ })
229+ }
230+ }
163231
164232 if decodedObject == nil && len (payload ) > 0 {
165233 err := json .Unmarshal (payload , & decodedObject )
166234 if err != nil {
167-
168- // cannot decode the request body, so it's not valid
169235 violation := & liberrors.SchemaValidationFailure {
170236 Reason : err .Error (),
171237 Location : "unavailable" ,
@@ -191,45 +257,12 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
191257 })
192258 return false , validationErrors
193259 }
194-
195- }
196-
197- path := ""
198- if schema .GoLow ().GetIndex () != nil {
199- path = schema .GoLow ().GetIndex ().GetSpecAbsolutePath ()
200260 }
201- jsch , err := helpers .NewCompiledSchemaWithVersion (path , jsonSchema , s .options , version )
202261
203262 var schemaValidationErrors []* liberrors.SchemaValidationFailure
204- if err != nil {
205- violation := & liberrors.SchemaValidationFailure {
206- Reason : err .Error (),
207- Location : "schema compilation" ,
208- ReferenceSchema : string (renderedSchema ),
209- ReferenceObject : string (payload ),
210- }
211- line := 1
212- col := 0
213- if schema .GoLow ().Type .KeyNode != nil {
214- line = schema .GoLow ().Type .KeyNode .Line
215- col = schema .GoLow ().Type .KeyNode .Column
216- }
217- validationErrors = append (validationErrors , & liberrors.ValidationError {
218- ValidationType : helpers .Schema ,
219- ValidationSubType : helpers .Schema ,
220- Message : "schema compilation failed" ,
221- Reason : fmt .Sprintf ("Schema compilation failed: %s" , err .Error ()),
222- SpecLine : line ,
223- SpecCol : col ,
224- SchemaValidationErrors : []* liberrors.SchemaValidationFailure {violation },
225- HowToFix : liberrors .HowToFixInvalidSchema ,
226- Context : string (renderedSchema ),
227- })
228- return false , validationErrors
229- }
230263
231- if jsch != nil && decodedObject != nil {
232- scErrs := jsch .Validate (decodedObject )
264+ if compiledSchema != nil && decodedObject != nil {
265+ scErrs := compiledSchema .Validate (decodedObject )
233266 if scErrs != nil {
234267
235268 var jk * jsonschema.ValidationError
@@ -238,7 +271,7 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
238271 // flatten the validationErrors
239272 schFlatErr := jk .BasicOutput ().Errors
240273 schemaValidationErrors = extractBasicErrors (schFlatErr , renderedSchema ,
241- decodedObject , payload , jk , schemaValidationErrors )
274+ renderedNode , decodedObject , payload , jk , schemaValidationErrors )
242275 }
243276 line := 1
244277 col := 0
@@ -266,13 +299,28 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
266299}
267300
268301func extractBasicErrors (schFlatErrs []jsonschema.OutputUnit ,
269- renderedSchema []byte , decodedObject interface {},
302+ renderedSchema []byte , renderedNode * yaml.Node ,
303+ decodedObject interface {},
270304 payload []byte , jk * jsonschema.ValidationError ,
271305 schemaValidationErrors []* liberrors.SchemaValidationFailure ,
272306) []* liberrors.SchemaValidationFailure {
273307 // Extract property name info once before processing errors (performance optimization)
274308 propertyInfo := extractPropertyNameFromError (jk )
275309
310+ // Determine root content node ONCE (not per-error).
311+ // NodeBuilder.Render() returns MappingNode directly, no DocumentNode unwrapping needed.
312+ var rootNode * yaml.Node
313+ if renderedNode != nil {
314+ rootNode = renderedNode
315+ } else if len (renderedSchema ) > 0 {
316+ // Fallback: parse bytes ONCE
317+ var docNode yaml.Node
318+ _ = yaml .Unmarshal (renderedSchema , & docNode )
319+ if len (docNode .Content ) > 0 {
320+ rootNode = docNode .Content [0 ]
321+ }
322+ }
323+
276324 for q := range schFlatErrs {
277325 er := schFlatErrs [q ]
278326
@@ -282,12 +330,11 @@ func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit,
282330 }
283331 if er .Error != nil {
284332
285- // re-encode the schema.
286- var renderedNode yaml.Node
287- _ = yaml .Unmarshal (renderedSchema , & renderedNode )
288-
289333 // locate the violated property in the schema
290- located := LocateSchemaPropertyNodeByJSONPath (renderedNode .Content [0 ], er .KeywordLocation )
334+ var located * yaml.Node
335+ if rootNode != nil {
336+ located = LocateSchemaPropertyNodeByJSONPath (rootNode , er .KeywordLocation )
337+ }
291338
292339 // extract the element specified by the instance
293340 val := instanceLocationRegex .FindStringSubmatch (er .InstanceLocation )
@@ -331,9 +378,9 @@ func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit,
331378 // location of the violation within the rendered schema.
332379 violation .Line = line
333380 violation .Column = located .Column
334- } else {
381+ } else if rootNode != nil {
335382 // handles property name validation errors that don't provide useful InstanceLocation
336- applyPropertyNameFallback (propertyInfo , renderedNode . Content [ 0 ] , violation )
383+ applyPropertyNameFallback (propertyInfo , rootNode , violation )
337384 }
338385 schemaValidationErrors = append (schemaValidationErrors , violation )
339386 }
0 commit comments