diff --git a/go.mod b/go.mod index b817197..b09d17c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/dlclark/regexp2 v1.11.5 github.com/goccy/go-yaml v1.19.1 github.com/pb33f/jsonpath v0.7.0 - github.com/pb33f/libopenapi v0.31.1 + github.com/pb33f/libopenapi v0.31.2 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v4 v4.0.0-rc.3 diff --git a/go.sum b/go.sum index a4a13c7..92b6450 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pb33f/jsonpath v0.7.0 h1:3oG6yu1RqNoMZpqnRjBMqi8fSIXWoDAKDrsB0QGTcoU= github.com/pb33f/jsonpath v0.7.0/go.mod h1:/+JlSIjWA2ijMVYGJ3IQPF4Q1nLMYbUTYNdk0exCDPQ= -github.com/pb33f/libopenapi v0.31.1 h1:smGr45U2Y+hHWYKiEV13oS2tP9IUnscqNb5qsvT9+YI= -github.com/pb33f/libopenapi v0.31.1/go.mod h1:oaebeA5l58AFbZ7qRKTtMnu15JEiPlaBas1vLDcw9vs= +github.com/pb33f/libopenapi v0.31.2 h1:dcFG9cPH7LvSejbemqqpSa3yrHYZs8eBHNdMx8ayIVc= +github.com/pb33f/libopenapi v0.31.2/go.mod h1:oaebeA5l58AFbZ7qRKTtMnu15JEiPlaBas1vLDcw9vs= github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ= github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index 0fded10..5740f1e 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -122,15 +122,15 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload return false, validationErrors } - // extract index of schema, and check the version - // schemaIndex := schema.GoLow().Index var renderedSchema []byte // render the schema, to be used for validation, stop this from running concurrently, mutations are made to state // and, it will cause async issues. // Create isolated render context for this validation to prevent false positive cycle detection // when multiple validations run concurrently. - renderCtx := base.NewInlineRenderContext() + // Use validation mode to force full inlining of discriminator refs - the JSON schema compiler + // needs a self-contained schema without unresolved $refs. + renderCtx := base.NewInlineRenderContextForValidation() s.lock.Lock() var e error renderedSchema, e = schema.RenderInlineWithContext(renderCtx) diff --git a/schema_validation/validate_schema_test.go b/schema_validation/validate_schema_test.go index 7c7229f..a414fda 100644 --- a/schema_validation/validate_schema_test.go +++ b/schema_validation/validate_schema_test.go @@ -1053,3 +1053,218 @@ components: }) } } + +// TestValidateSchema_Discriminator_OneOf_WithRefs_Issue788 tests that validation works correctly +// when a schema has discriminator + oneOf with $ref to component schemas. +// This was a regression in vacuum v0.21.2+ where the validator would fail with +// "JSON schema compile failed: json-pointer not found" because discriminator refs +// were being preserved instead of inlined. +// https://github.com/daveshanley/vacuum/issues/788 +func TestValidateSchema_Discriminator_OneOf_WithRefs_Issue788(t *testing.T) { + spec := `openapi: 3.1.0 +info: + title: Test Discriminator OneOf With Refs + version: 1.0.0 +components: + schemas: + ProductWidget: + type: object + required: + - productName + - quantity + - color + properties: + productName: + type: string + enum: + - Widget + quantity: + type: integer + minimum: 1 + color: + type: string + enum: + - Red + - Blue + - Green + ProductGadget: + type: object + required: + - productName + - quantity + - size + properties: + productName: + type: string + enum: + - Gadget + quantity: + type: integer + minimum: 1 + size: + type: string + enum: + - Small + - Medium + - Large + Product: + oneOf: + - $ref: '#/components/schemas/ProductWidget' + - $ref: '#/components/schemas/ProductGadget' + discriminator: + propertyName: productName` + + doc, err := libopenapi.NewDocument([]byte(spec)) + assert.NoError(t, err) + + model, errs := doc.BuildV3Model() + assert.Empty(t, errs) + + productSchema := model.Model.Components.Schemas.GetOrZero("Product").Schema() + + // Valid Widget product + validWidget := map[string]interface{}{ + "productName": "Widget", + "quantity": 1, + "color": "Green", + } + + validator := NewSchemaValidator() + valid, validationErrors := validator.ValidateSchemaObject(productSchema, validWidget) + + // This should pass without "json-pointer not found" error + assert.True(t, valid, "validation should pass for valid product with discriminator oneOf refs") + assert.Empty(t, validationErrors, "no validation errors should be present") +} + +// TestValidateSchema_Discriminator_AnyOf_WithRefs tests anyOf with discriminator and $refs +func TestValidateSchema_Discriminator_AnyOf_WithRefs(t *testing.T) { + spec := `openapi: 3.1.0 +info: + title: Test Discriminator AnyOf With Refs + version: 1.0.0 +components: + schemas: + Cat: + type: object + required: + - petType + - meow + properties: + petType: + type: string + const: cat + meow: + type: boolean + Dog: + type: object + required: + - petType + - bark + properties: + petType: + type: string + const: dog + bark: + type: boolean + Pet: + anyOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + discriminator: + propertyName: petType` + + doc, err := libopenapi.NewDocument([]byte(spec)) + assert.NoError(t, err) + + model, errs := doc.BuildV3Model() + assert.Empty(t, errs) + + petSchema := model.Model.Components.Schemas.GetOrZero("Pet").Schema() + + // Valid cat + validCat := map[string]interface{}{ + "petType": "cat", + "meow": true, + } + + validator := NewSchemaValidator() + valid, validationErrors := validator.ValidateSchemaObject(petSchema, validCat) + + assert.True(t, valid, "validation should pass for valid cat with discriminator anyOf refs") + assert.Empty(t, validationErrors, "no validation errors should be present") +} + +// TestValidateSchema_Discriminator_OneOf_WithRefs_InvalidData tests that invalid data +// still fails validation correctly (not a false negative) +func TestValidateSchema_Discriminator_OneOf_WithRefs_InvalidData(t *testing.T) { + spec := `openapi: 3.1.0 +info: + title: Test Discriminator OneOf Invalid + version: 1.0.0 +components: + schemas: + ProductWidget: + type: object + required: + - productName + - quantity + - color + properties: + productName: + type: string + enum: + - Widget + quantity: + type: integer + minimum: 1 + color: + type: string + enum: + - Red + - Blue + ProductGadget: + type: object + required: + - productName + - quantity + - size + properties: + productName: + type: string + enum: + - Gadget + quantity: + type: integer + minimum: 1 + size: + type: string + Product: + oneOf: + - $ref: '#/components/schemas/ProductWidget' + - $ref: '#/components/schemas/ProductGadget' + discriminator: + propertyName: productName` + + doc, err := libopenapi.NewDocument([]byte(spec)) + assert.NoError(t, err) + + model, errs := doc.BuildV3Model() + assert.Empty(t, errs) + + productSchema := model.Model.Components.Schemas.GetOrZero("Product").Schema() + + // Invalid product - missing required field 'color' for Widget + invalidProduct := map[string]interface{}{ + "productName": "Widget", + "quantity": 1, + // missing required 'color' field + } + + validator := NewSchemaValidator() + valid, validationErrors := validator.ValidateSchemaObject(productSchema, invalidProduct) + + // This should fail because 'color' is required for Widget + assert.False(t, valid, "validation should fail for invalid product") + assert.NotEmpty(t, validationErrors, "validation errors should be present") +}