-
Notifications
You must be signed in to change notification settings - Fork 47
Expand file tree
/
Copy pathvalidate_document.go
More file actions
171 lines (148 loc) · 6.39 KB
/
validate_document.go
File metadata and controls
171 lines (148 loc) · 6.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley
// SPDX-License-Identifier: MIT
package schema_validation
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/pb33f/libopenapi"
"github.com/santhosh-tekuri/jsonschema/v6"
"go.yaml.in/yaml/v4"
"golang.org/x/text/language"
"golang.org/x/text/message"
"github.com/pb33f/libopenapi-validator/config"
liberrors "github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
)
func normalizeJSON(data any) any {
d, _ := json.Marshal(data)
var normalized any
_ = json.Unmarshal(d, &normalized)
return normalized
}
// ValidateOpenAPIDocument will validate an OpenAPI document against the OpenAPI 2, 3.0 and 3.1 schemas (depending on version)
// It will return true if the document is valid, false if it is not and a slice of ValidationError pointers.
func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bool, []*liberrors.ValidationError) {
return ValidateOpenAPIDocumentWithPrecompiled(doc, nil, opts...)
}
// ValidateOpenAPIDocumentWithPrecompiled validates an OpenAPI document against the OAS JSON Schema.
// When compiledSchema is non-nil it is used directly, skipping schema compilation.
// When SpecJSONBytes is available on the document's SpecInfo, the normalizeJSON round-trip is
// bypassed in favour of a single jsonschema.UnmarshalJSON call.
func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSchema *jsonschema.Schema, opts ...config.Option) (bool, []*liberrors.ValidationError) {
options := config.NewValidationOptions(opts...)
info := doc.GetSpecInfo()
loadedSchema := info.APISchema
var validationErrors []*liberrors.ValidationError
// Check if both JSON representations are nil before proceeding
if info.SpecJSON == nil && info.SpecJSONBytes == nil {
validationErrors = append(validationErrors, &liberrors.ValidationError{
ValidationType: helpers.Schema,
ValidationSubType: "document",
Message: "OpenAPI document validation failed",
Reason: "The document's SpecJSON is nil, indicating the document was not properly parsed or is empty",
SpecLine: 1,
SpecCol: 0,
HowToFix: "ensure the OpenAPI document is valid YAML/JSON and can be properly parsed by libopenapi",
Context: "document root",
})
return false, validationErrors
}
// Use the precompiled schema if provided, otherwise compile it
jsch := compiledSchema
if jsch == nil {
var err error
jsch, err = helpers.NewCompiledSchema("schema", []byte(loadedSchema), options)
if err != nil {
validationErrors = append(validationErrors, &liberrors.ValidationError{
ValidationType: helpers.Schema,
ValidationSubType: "compilation",
Message: "OpenAPI document schema compilation failed",
Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()),
SpecLine: 1,
SpecCol: 0,
HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs",
Context: loadedSchema,
})
return false, validationErrors
}
}
// Build the normalized document value for validation.
// Prefer SpecJSONBytes (single unmarshal) over SpecJSON (marshal+unmarshal round-trip).
var normalized any
if info.SpecJSONBytes != nil && len(*info.SpecJSONBytes) > 0 {
var err error
normalized, err = jsonschema.UnmarshalJSON(bytes.NewReader(*info.SpecJSONBytes))
if err != nil {
// Fall back to normalizeJSON if UnmarshalJSON fails
if info.SpecJSON != nil {
normalized = normalizeJSON(*info.SpecJSON)
}
}
} else if info.SpecJSON != nil {
normalized = normalizeJSON(*info.SpecJSON)
}
// Validate the document
scErrs := jsch.Validate(normalized)
var schemaValidationErrors []*liberrors.SchemaValidationFailure
if scErrs != nil {
var jk *jsonschema.ValidationError
if errors.As(scErrs, &jk) {
// flatten the validationErrors
schFlatErrs := jk.BasicOutput().Errors
// Extract property name info once before processing errors (performance optimization)
propertyInfo := extractPropertyNameFromError(jk)
for q := range schFlatErrs {
er := schFlatErrs[q]
errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{}))
if er.KeywordLocation == "" || helpers.IgnorePolyRegex.MatchString(errMsg) {
continue // ignore this error, it's useless tbh, utter noise.
}
if errMsg != "" {
// locate the violated property in the schema
located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation)
violation := &liberrors.SchemaValidationFailure{
Reason: errMsg,
FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation),
FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation),
InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation),
KeywordLocation: er.KeywordLocation,
OriginalJsonSchemaError: jk,
}
// if we have a location within the schema, add it to the error
if located != nil {
line := located.Line
// if the located node is a map or an array, then the actual human interpretable
// line on which the violation occurred is the line of the key, not the value.
if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode {
if line > 0 {
line--
}
}
// location of the violation within the rendered schema.
violation.Line = line
violation.Column = located.Column
} else {
// handles property name validation errors that don't provide useful InstanceLocation
applyPropertyNameFallback(propertyInfo, info.RootNode.Content[0], violation)
}
schemaValidationErrors = append(schemaValidationErrors, violation)
}
}
}
// add the error to the list
validationErrors = append(validationErrors, &liberrors.ValidationError{
ValidationType: helpers.Schema,
Message: "Document does not pass validation",
Reason: fmt.Sprintf("OpenAPI document is not valid according "+
"to the %s specification", info.Version),
SchemaValidationErrors: schemaValidationErrors,
HowToFix: liberrors.HowToFixInvalidSchema,
})
}
if len(validationErrors) > 0 {
return false, validationErrors
}
return true, nil
}