-
Notifications
You must be signed in to change notification settings - Fork 43
Open
Description
Apologies, this is a real bug I'm facing, but the investigation was largely Claude Code's :D.
The tl;dr is I have a spec that uses oneOf with a discriminator, and I'm getting validation errors at runtime (when validating the response), with errors complaining about the json pointers being missing.
Claude suggests that the main issue is that in a few cases, we should use : base.NewInlineRenderContextForValidation() instead of base.NewInlineRenderContext()
It suggested these places should be changed:
The fix is trivial. It's literally a one-line change in each location:
// Before
renderCtx := base.NewInlineRenderContext()
// After
renderCtx := base.NewInlineRenderContextForValidation()
Fix Summary
┌──────────────────────────────────┬─────────────────┐
│ File │ Lines to change │
├──────────────────────────────────┼─────────────────┤
│ responses/validate_response.go │ Line 84 │
├──────────────────────────────────┼─────────────────┤
│ requests/validate_request.go │ Line 81 │
├──────────────────────────────────┼─────────────────┤
│ parameters/validate_parameter.go │ Lines 97, 237 │
├──────────────────────────────────┼─────────────────┤
│ validator.go │ Lines 488, 541 │
├──────────────────────────────────┼─────────────────┤
│ strict/types.go │ Line 333 │
└──────────────────────────────────┴─────────────────┘
Total: 7 one-line changes (all identical substitutions)
Here is a sample test that fails, and in theory shows that switching the method gets rid of the weird refs.
package validator
import (
"io"
"net/http"
"strings"
"testing"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/datamodel/high/base"
)
// Minimal OpenAPI spec with oneOf + discriminator pattern that triggers the bug
const discriminatorSpec = `openapi: "3.1.0"
info:
title: "Test"
version: "1.0.0"
components:
schemas:
BaseField:
type: object
properties:
id:
type: string
field_type:
type: string
BooleanField:
allOf:
- $ref: '#/components/schemas/BaseField'
- type: object
properties:
default_value:
type: boolean
StringField:
allOf:
- $ref: '#/components/schemas/BaseField'
- type: object
properties:
max_length:
type: integer
Field:
oneOf:
- $ref: '#/components/schemas/BooleanField'
- $ref: '#/components/schemas/StringField'
discriminator:
propertyName: field_type
mapping:
boolean: '#/components/schemas/BooleanField'
string: '#/components/schemas/StringField'
paths:
/fields:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Field'
`
// TestValidateHttpResponse_FailsWithDiscriminatorOneOf demonstrates the actual
// user-facing bug: ValidateHttpResponse fails with "json-pointer ... not found"
// when validating responses against schemas using oneOf with discriminator.
//
// The response body is completely valid according to the schema, but validation
// fails because the schema renderer uses Bundle mode which preserves $ref in
// discriminator oneOf schemas. The JSON Schema compiler then can't resolve
// these refs because it's treating the rendered schema as a standalone document.
func TestValidateHttpResponse_FailsWithDiscriminatorOneOf(t *testing.T) {
// Valid response body that matches BooleanField schema
responseBody := `{
"id": "field-1",
"field_type": "boolean",
"default_value": true
}`
doc, err := libopenapi.NewDocument([]byte(discriminatorSpec))
if err != nil {
t.Fatalf("Failed to parse spec: %v", err)
}
v, errs := NewValidator(doc)
if len(errs) > 0 {
t.Fatalf("Failed to create validator: %v", errs)
}
request, _ := http.NewRequest(http.MethodGet, "/fields", nil)
response := &http.Response{
StatusCode: 200,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(responseBody)),
}
valid, validationErrs := v.ValidateHttpResponse(request, response)
// Check if we hit the json-pointer bug
for _, ve := range validationErrs {
if strings.Contains(ve.Message, "json-pointer") ||
strings.Contains(ve.Reason, "json-pointer") {
t.Logf("BUG: Response validation failed with json-pointer error")
t.Logf("Message: %s", ve.Message)
t.Logf("Reason: %s", ve.Reason)
t.Log("")
t.Log("ROOT CAUSE: validate_response.go uses NewInlineRenderContext() (Bundle mode)")
t.Log("which preserves $ref in discriminator oneOf schemas.")
t.Log("")
t.Log("FIX: Change to NewInlineRenderContextForValidation() which inlines all $refs")
t.FailNow()
}
}
if !valid {
t.Logf("Validation failed with other errors: %v", validationErrs)
} else {
t.Log("Validation passed - bug may be fixed!")
}
}
// TestRenderInlineContext_DiscriminatorOneOf demonstrates the root cause:
// NewInlineRenderContext() (Bundle mode) preserves $ref in discriminator oneOf,
// while NewInlineRenderContextForValidation() correctly inlines everything.
//
// See libopenapi schema_proxy.go:74-107 for the two rendering modes:
// - RenderingModeBundle (NewInlineRenderContext): preserves discriminator $refs
// - RenderingModeValidation (NewInlineRenderContextForValidation): inlines everything
func TestRenderInlineContext_DiscriminatorOneOf(t *testing.T) {
doc, err := libopenapi.NewDocument([]byte(discriminatorSpec))
if err != nil {
t.Fatalf("Failed to parse spec: %v", err)
}
model, errs := doc.BuildV3Model()
if errs != nil {
t.Fatalf("Failed to build model: %v", errs)
}
// Get the response schema
pathItem := model.Model.Paths.PathItems.GetOrZero("/fields")
response := pathItem.Get.Responses.Codes.GetOrZero("200")
mediaType := response.Content.GetOrZero("application/json")
schema := mediaType.Schema.Schema()
t.Run("BundleMode_PreservesRefs_CausesValidationFailure", func(t *testing.T) {
// This is what libopenapi-validator currently uses in validate_response.go
renderCtx := base.NewInlineRenderContext()
rendered, err := schema.RenderInlineWithContext(renderCtx)
if err != nil {
t.Fatalf("RenderInlineWithContext failed: %v", err)
}
renderedStr := string(rendered)
t.Logf("Bundle mode rendered schema:\n%s", renderedStr)
// Bundle mode intentionally preserves $ref in discriminator oneOf
// This causes JSON Schema compilation to fail with:
// "json-pointer in file:///...#/components/schemas/BooleanField not found"
if !strings.Contains(renderedStr, "$ref") {
t.Error("Expected Bundle mode to preserve $ref, but none found")
} else {
refCount := strings.Count(renderedStr, "$ref")
t.Logf("Bundle mode preserved %d $ref (as designed)", refCount)
t.Log("This is the ROOT CAUSE: passing this to JSON Schema compiler fails")
}
})
t.Run("ValidationMode_InlinesAllRefs_FixesValidation", func(t *testing.T) {
// This is what libopenapi-validator SHOULD use
renderCtx := base.NewInlineRenderContextForValidation()
rendered, err := schema.RenderInlineWithContext(renderCtx)
if err != nil {
t.Fatalf("RenderInlineWithContext failed: %v", err)
}
renderedStr := string(rendered)
t.Logf("Validation mode rendered schema:\n%s", renderedStr)
// Validation mode should fully inline all $ref
if strings.Contains(renderedStr, "$ref") {
refCount := strings.Count(renderedStr, "$ref")
t.Errorf("Validation mode should inline all $ref, but found %d", refCount)
} else {
t.Log("Validation mode correctly inlined all $ref")
t.Log("This schema can be passed to JSON Schema compiler successfully")
}
})
}
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels