44package responses
55
66import (
7- "bytes"
8- "encoding/json"
9- "fmt"
10- "github.com/pb33f/libopenapi-validator/errors"
11- "github.com/pb33f/libopenapi-validator/helpers"
12- "github.com/pb33f/libopenapi-validator/schema_validation"
13- "github.com/pb33f/libopenapi/datamodel/high/base"
14- "github.com/santhosh-tekuri/jsonschema/v5"
15- "gopkg.in/yaml.v3"
16- "io"
17- "net/http"
18- "reflect"
19- "regexp"
20- "strconv"
21- "strings"
7+ "bytes"
8+ "encoding/json"
9+ "fmt"
10+ "github.com/pb33f/libopenapi-validator/errors"
11+ "github.com/pb33f/libopenapi-validator/helpers"
12+ "github.com/pb33f/libopenapi-validator/schema_validation"
13+ "github.com/pb33f/libopenapi/datamodel/high/base"
14+ "github.com/santhosh-tekuri/jsonschema/v5"
15+ "gopkg.in/yaml.v3"
16+ "io"
17+ "net/http"
18+ "reflect"
19+ "regexp"
20+ "strconv"
21+ "strings"
2222)
2323
2424var instanceLocationRegex = regexp .MustCompile (`^/(\d+)` )
@@ -29,125 +29,124 @@ var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`)
2929//
3030// This function is used by the ValidateResponseBody function, but can be used independently.
3131func ValidateResponseSchema (
32- request * http.Request ,
33- response * http.Response ,
34- schema * base.Schema ,
35- renderedSchema ,
36- jsonSchema []byte ) (bool , []* errors.ValidationError ) {
37-
38- var validationErrors []* errors.ValidationError
39-
40- responseBody , _ := io .ReadAll (response .Body )
41-
42- // close the request body, so it can be re-read later by another player in the chain
43- _ = response .Body .Close ()
44- response .Body = io .NopCloser (bytes .NewBuffer (responseBody ))
45-
46- var decodedObj interface {}
47- _ = json .Unmarshal (responseBody , & decodedObj )
48-
49- // no response body? failed to decode anything? nothing to do here.
50- if responseBody == nil || decodedObj == nil {
51- return true , nil
52- }
53-
54- // create a new jsonschema compiler and add in the rendered JSON schema.
55- compiler := jsonschema .NewCompiler ()
56- fName := fmt .Sprintf ("%s.json" , helpers .ResponseBodyValidation )
57- _ = compiler .AddResource (fName ,
58- strings .NewReader (string (jsonSchema )))
59- jsch , _ := compiler .Compile (fName )
60-
61- // validate the object against the schema
62- scErrs := jsch .Validate (decodedObj )
63- if scErrs != nil {
64- jk := scErrs .(* jsonschema.ValidationError )
65-
66- // flatten the validationErrors
67- schFlatErrs := jk .BasicOutput ().Errors
68- var schemaValidationErrors []* errors.SchemaValidationFailure
69- for q := range schFlatErrs {
70- er := schFlatErrs [q ]
71- if er .KeywordLocation == "" || strings .HasPrefix (er .Error , "doesn't validate with" ) {
72- continue // ignore this error, it's useless tbh, utter noise.
73- }
74- if er .Error != "" {
75-
76- // re-encode the schema.
77- var renderedNode yaml.Node
78- _ = yaml .Unmarshal (renderedSchema , & renderedNode )
79-
80- // locate the violated property in the schema
81- located := schema_validation .LocateSchemaPropertyNodeByJSONPath (renderedNode .Content [0 ], er .KeywordLocation )
82-
83- // extract the element specified by the instance
84- val := instanceLocationRegex .FindStringSubmatch (er .InstanceLocation )
85- var referenceObject string
86-
87- if len (val ) > 0 {
88- referenceIndex , _ := strconv .Atoi (val [1 ])
89- if reflect .ValueOf (decodedObj ).Type ().Kind () == reflect .Slice {
90- found := decodedObj .([]any )[referenceIndex ]
91- recoded , _ := json .MarshalIndent (found , "" , " " )
92- referenceObject = string (recoded )
93- }
94- }
95- if referenceObject == "" {
96- referenceObject = string (responseBody )
97- }
98-
99- violation := & errors.SchemaValidationFailure {
100- Reason : er .Error ,
101- Location : er .KeywordLocation ,
102- ReferenceSchema : string (renderedSchema ),
103- ReferenceObject : referenceObject ,
104- OriginalError : jk ,
105- }
106- // if we have a location within the schema, add it to the error
107- if located != nil {
108-
109- line := located .Line
110- // if the located node is a map or an array, then the actual human interpretable
111- // line on which the violation occurred is the line of the key, not the value.
112- if located .Kind == yaml .MappingNode || located .Kind == yaml .SequenceNode {
113- if line > 0 {
114- line --
115- }
116- }
117-
118- // location of the violation within the rendered schema.
119- violation .Line = line
120- violation .Column = located .Column
121- }
122- schemaValidationErrors = append (schemaValidationErrors , violation )
123- }
124- }
125-
126- line := 0
127- col := 0
128- if schema .GoLow ().Type .KeyNode != nil {
129- line = schema .GoLow ().Type .KeyNode .Line
130- col = schema .GoLow ().Type .KeyNode .Column
131- }
132-
133-
134- // add the error to the list
135- validationErrors = append (validationErrors , & errors.ValidationError {
136- ValidationType : helpers .ResponseBodyValidation ,
137- ValidationSubType : helpers .Schema ,
138- Message : fmt .Sprintf ("%d response body for '%s' failed to validate schema" ,
139- response .StatusCode , request .URL .Path ),
140- Reason : fmt .Sprintf ("The response body for status code '%d' is defined as an object. " +
141- "However, it does not meet the schema requirements of the specification" , response .StatusCode ),
142- SpecLine : line ,
143- SpecCol : col ,
144- SchemaValidationErrors : schemaValidationErrors ,
145- HowToFix : errors .HowToFixInvalidSchema ,
146- Context : string (renderedSchema ), // attach the rendered schema to the error
147- })
148- }
149- if len (validationErrors ) > 0 {
150- return false , validationErrors
151- }
152- return true , nil
32+ request * http.Request ,
33+ response * http.Response ,
34+ schema * base.Schema ,
35+ renderedSchema ,
36+ jsonSchema []byte ) (bool , []* errors.ValidationError ) {
37+
38+ var validationErrors []* errors.ValidationError
39+
40+ responseBody , _ := io .ReadAll (response .Body )
41+
42+ // close the request body, so it can be re-read later by another player in the chain
43+ _ = response .Body .Close ()
44+ response .Body = io .NopCloser (bytes .NewBuffer (responseBody ))
45+
46+ var decodedObj interface {}
47+ _ = json .Unmarshal (responseBody , & decodedObj )
48+
49+ // no response body? failed to decode anything? nothing to do here.
50+ if responseBody == nil || decodedObj == nil {
51+ return true , nil
52+ }
53+
54+ // create a new jsonschema compiler and add in the rendered JSON schema.
55+ compiler := jsonschema .NewCompiler ()
56+ fName := fmt .Sprintf ("%s.json" , helpers .ResponseBodyValidation )
57+ _ = compiler .AddResource (fName ,
58+ strings .NewReader (string (jsonSchema )))
59+ jsch , _ := compiler .Compile (fName )
60+
61+ // validate the object against the schema
62+ scErrs := jsch .Validate (decodedObj )
63+ if scErrs != nil {
64+ jk := scErrs .(* jsonschema.ValidationError )
65+
66+ // flatten the validationErrors
67+ schFlatErrs := jk .BasicOutput ().Errors
68+ var schemaValidationErrors []* errors.SchemaValidationFailure
69+ for q := range schFlatErrs {
70+ er := schFlatErrs [q ]
71+ if er .KeywordLocation == "" || strings .HasPrefix (er .Error , "doesn't validate with" ) {
72+ continue // ignore this error, it's useless tbh, utter noise.
73+ }
74+ if er .Error != "" {
75+
76+ // re-encode the schema.
77+ var renderedNode yaml.Node
78+ _ = yaml .Unmarshal (renderedSchema , & renderedNode )
79+
80+ // locate the violated property in the schema
81+ located := schema_validation .LocateSchemaPropertyNodeByJSONPath (renderedNode .Content [0 ], er .KeywordLocation )
82+
83+ // extract the element specified by the instance
84+ val := instanceLocationRegex .FindStringSubmatch (er .InstanceLocation )
85+ var referenceObject string
86+
87+ if len (val ) > 0 {
88+ referenceIndex , _ := strconv .Atoi (val [1 ])
89+ if reflect .ValueOf (decodedObj ).Type ().Kind () == reflect .Slice {
90+ found := decodedObj .([]any )[referenceIndex ]
91+ recoded , _ := json .MarshalIndent (found , "" , " " )
92+ referenceObject = string (recoded )
93+ }
94+ }
95+ if referenceObject == "" {
96+ referenceObject = string (responseBody )
97+ }
98+
99+ violation := & errors.SchemaValidationFailure {
100+ Reason : er .Error ,
101+ Location : er .KeywordLocation ,
102+ ReferenceSchema : string (renderedSchema ),
103+ ReferenceObject : referenceObject ,
104+ OriginalError : jk ,
105+ }
106+ // if we have a location within the schema, add it to the error
107+ if located != nil {
108+
109+ line := located .Line
110+ // if the located node is a map or an array, then the actual human interpretable
111+ // line on which the violation occurred is the line of the key, not the value.
112+ if located .Kind == yaml .MappingNode || located .Kind == yaml .SequenceNode {
113+ if line > 0 {
114+ line --
115+ }
116+ }
117+
118+ // location of the violation within the rendered schema.
119+ violation .Line = line
120+ violation .Column = located .Column
121+ }
122+ schemaValidationErrors = append (schemaValidationErrors , violation )
123+ }
124+ }
125+
126+ line := 1
127+ col := 0
128+ if schema .GoLow ().Type .KeyNode != nil {
129+ line = schema .GoLow ().Type .KeyNode .Line
130+ col = schema .GoLow ().Type .KeyNode .Column
131+ }
132+
133+ // add the error to the list
134+ validationErrors = append (validationErrors , & errors.ValidationError {
135+ ValidationType : helpers .ResponseBodyValidation ,
136+ ValidationSubType : helpers .Schema ,
137+ Message : fmt .Sprintf ("%d response body for '%s' failed to validate schema" ,
138+ response .StatusCode , request .URL .Path ),
139+ Reason : fmt .Sprintf ("The response body for status code '%d' is defined as an object. " +
140+ "However, it does not meet the schema requirements of the specification" , response .StatusCode ),
141+ SpecLine : line ,
142+ SpecCol : col ,
143+ SchemaValidationErrors : schemaValidationErrors ,
144+ HowToFix : errors .HowToFixInvalidSchema ,
145+ Context : string (renderedSchema ), // attach the rendered schema to the error
146+ })
147+ }
148+ if len (validationErrors ) > 0 {
149+ return false , validationErrors
150+ }
151+ return true , nil
153152}
0 commit comments