Skip to content

Commit 5dcf0b4

Browse files
committed
feat: add document-scoped format validators to prevent global state pollution
Add support for per-document format validators that are scoped to individual OpenAPI specs instead of being shared globally. This solves the problem where multiple specs in the same application cannot have different validation rules for the same format name. Problem: DefineStringFormatValidator() uses global maps, causing issues when: - Multiple OpenAPI specs are loaded in the same application - Different specs need different validation logic for the same format name - One spec's validator registration overwrites another's globally Solution: - Add format validator maps to openapi3.T (document-level storage) - Add SetStringFormatValidator(), SetNumberFormatValidator(), and SetIntegerFormatValidator() methods to openapi3.T - Add ValidateSchemaJSON() convenience method that automatically applies the document's validators - Update schema validation logic to check: per-validation validators → document-scoped validators → global validators Usage: specA := loader.LoadFromFile("spec-a.yaml") specA.SetStringFormatValidator("custom-id", validatorA) specB := loader.LoadFromFile("spec-b.yaml") specB.SetStringFormatValidator("custom-id", validatorB) // Each spec uses its own validators - no conflicts! err := specA.ValidateSchemaJSON(schemaA, value) err := specB.ValidateSchemaJSON(schemaB, value) Changes: - openapi3/openapi3.go: Add validator fields and methods to T - openapi3/schema_validation_settings.go: Add per-validation validator options - openapi3/schema.go: Update validation logic to check all validator sources - openapi3/schema_formats_test.go: Add comprehensive tests This change is fully backwards compatible. Existing code using global validators continues to work unchanged.
1 parent 45db2ad commit 5dcf0b4

File tree

4 files changed

+457
-3
lines changed

4 files changed

+457
-3
lines changed

openapi3/openapi3.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ type T struct {
2626

2727
visited visitedComponent
2828
url *url.URL
29+
30+
// Document-scoped format validators
31+
// These validators are automatically used by all schemas in this document
32+
stringFormats map[string]StringFormatValidator
33+
numberFormats map[string]NumberFormatValidator
34+
integerFormats map[string]IntegerFormatValidator
2935
}
3036

3137
var _ jsonpointer.JSONPointable = (*T)(nil)
@@ -137,6 +143,64 @@ func (doc *T) AddServers(servers ...*Server) {
137143
doc.Servers = append(doc.Servers, servers...)
138144
}
139145

146+
// SetStringFormatValidators sets document-scoped string format validators.
147+
// These validators are automatically used by all schemas in this document.
148+
func (doc *T) SetStringFormatValidators(validators map[string]StringFormatValidator) {
149+
doc.stringFormats = validators
150+
}
151+
152+
// SetStringFormatValidator sets a single document-scoped string format validator.
153+
func (doc *T) SetStringFormatValidator(name string, validator StringFormatValidator) {
154+
if doc.stringFormats == nil {
155+
doc.stringFormats = make(map[string]StringFormatValidator)
156+
}
157+
doc.stringFormats[name] = validator
158+
}
159+
160+
// SetNumberFormatValidators sets document-scoped number format validators.
161+
// These validators are automatically used by all schemas in this document.
162+
func (doc *T) SetNumberFormatValidators(validators map[string]NumberFormatValidator) {
163+
doc.numberFormats = validators
164+
}
165+
166+
// SetNumberFormatValidator sets a single document-scoped number format validator.
167+
func (doc *T) SetNumberFormatValidator(name string, validator NumberFormatValidator) {
168+
if doc.numberFormats == nil {
169+
doc.numberFormats = make(map[string]NumberFormatValidator)
170+
}
171+
doc.numberFormats[name] = validator
172+
}
173+
174+
// SetIntegerFormatValidators sets document-scoped integer format validators.
175+
// These validators are automatically used by all schemas in this document.
176+
func (doc *T) SetIntegerFormatValidators(validators map[string]IntegerFormatValidator) {
177+
doc.integerFormats = validators
178+
}
179+
180+
// SetIntegerFormatValidator sets a single document-scoped integer format validator.
181+
func (doc *T) SetIntegerFormatValidator(name string, validator IntegerFormatValidator) {
182+
if doc.integerFormats == nil {
183+
doc.integerFormats = make(map[string]IntegerFormatValidator)
184+
}
185+
doc.integerFormats[name] = validator
186+
}
187+
188+
// GetSchemaValidationOptions returns SchemaValidationOptions that include
189+
// this document's format validators. Use this when validating schemas from this document.
190+
func (doc *T) GetSchemaValidationOptions() []SchemaValidationOption {
191+
var opts []SchemaValidationOption
192+
if doc.stringFormats != nil {
193+
opts = append(opts, WithStringFormatValidators(doc.stringFormats))
194+
}
195+
if doc.numberFormats != nil {
196+
opts = append(opts, WithNumberFormatValidators(doc.numberFormats))
197+
}
198+
if doc.integerFormats != nil {
199+
opts = append(opts, WithIntegerFormatValidators(doc.integerFormats))
200+
}
201+
return opts
202+
}
203+
140204
// Validate returns an error if T does not comply with the OpenAPI spec.
141205
// Validations Options can be provided to modify the validation behavior.
142206
func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error {
@@ -203,3 +267,11 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error {
203267

204268
return validateExtensions(ctx, doc.Extensions)
205269
}
270+
271+
// ValidateSchemaJSON validates data against a schema using this document's format validators.
272+
// This is a convenience method that automatically applies the document's format validators.
273+
func (doc *T) ValidateSchemaJSON(schema *Schema, value any, opts ...SchemaValidationOption) error {
274+
// Combine document's validators with any additional options
275+
allOpts := append(doc.GetSchemaValidationOptions(), opts...)
276+
return schema.VisitJSON(value, allOpts...)
277+
}

openapi3/schema.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,7 +1527,12 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
15271527
format := schema.Format
15281528
if format != "" {
15291529
if requireInteger {
1530-
if f, ok := SchemaIntegerFormats[format]; ok {
1530+
// Check per-validation validators first, then fall back to global
1531+
f, ok := settings.integerFormats[format]
1532+
if !ok {
1533+
f, ok = SchemaIntegerFormats[format]
1534+
}
1535+
if ok {
15311536
if err := f.Validate(int64(value)); err != nil {
15321537
var reason string
15331538
schemaErr := &SchemaError{}
@@ -1541,7 +1546,12 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
15411546
}
15421547
}
15431548
} else {
1544-
if f, ok := SchemaNumberFormats[format]; ok {
1549+
// Check per-validation validators first, then fall back to global
1550+
f, ok := settings.numberFormats[format]
1551+
if !ok {
1552+
f, ok = SchemaNumberFormats[format]
1553+
}
1554+
if ok {
15451555
if err := f.Validate(value); err != nil {
15461556
var reason string
15471557
schemaErr := &SchemaError{}
@@ -1767,7 +1777,12 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
17671777
var formatStrErr string
17681778
var formatErr error
17691779
if format := schema.Format; format != "" {
1770-
if f, ok := SchemaStringFormats[format]; ok {
1780+
// Check per-validation validators first, then fall back to global
1781+
f, ok := settings.stringFormats[format]
1782+
if !ok {
1783+
f, ok = SchemaStringFormats[format]
1784+
}
1785+
if ok {
17711786
if err := f.Validate(value); err != nil {
17721787
var reason string
17731788
schemaErr := &SchemaError{}

0 commit comments

Comments
 (0)