Files
Stephan Renatus 1661f22ba3 ast: add some schema $ref tests
Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
2026-05-28 09:49:16 +02:00

1845 lines
49 KiB
Go

// Copyright 2021 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package ast
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/open-policy-agent/opa/v1/types"
"github.com/open-policy-agent/opa/v1/util"
)
func testParseSchema(t *testing.T, schema string, expectedType types.Type, expectedError error) {
t.Helper()
var sch any
err := util.Unmarshal([]byte(schema), &sch)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
newtype, err := loadSchema(sch, nil)
if err != nil && errors.Is(err, expectedError) {
t.Fatalf("unexpected error: %v", err)
}
if newtype == nil && expectedType != nil {
t.Fatalf("parseSchema returned nil type")
}
if newtype != nil && expectedType == nil {
t.Fatalf("expected nil but parseSchema returned a not nil type")
}
if types.Compare(newtype, expectedType) != 0 {
t.Fatalf("parseSchema returned an incorrect type: %s, expected: %s", newtype.String(), expectedType.String())
}
}
func TestParseSchemaObject(t *testing.T) {
innerObjectStaticProps := append([]*types.StaticProperty{},
&types.StaticProperty{Key: "a", Value: types.N},
&types.StaticProperty{Key: "b", Value: types.NewArray(nil, types.N)},
&types.StaticProperty{Key: "c", Value: types.A},
)
staticProps := append([]*types.StaticProperty{},
&types.StaticProperty{Key: "b", Value: types.NewArray(nil, types.NewObject(innerObjectStaticProps, nil))},
&types.StaticProperty{Key: "foo", Value: types.S},
)
expectedType := types.NewObject(staticProps, nil)
testParseSchema(t, objectSchema, expectedType, nil)
}
func TestSetTypesWithSchemaRef(t *testing.T) {
var sch any
ts := kubeSchemaServer(t)
t.Cleanup(ts.Close)
refSchemaReplaced := strings.ReplaceAll(refSchema, "https://kubernetesjsonschema.dev/v1.14.0/", ts.URL+"/")
err := util.Unmarshal([]byte(refSchemaReplaced), &sch)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
t.Run("remote refs disabled", func(t *testing.T) {
_, err := loadSchema(sch, []string{})
if err == nil {
t.Fatal("expected error, got nil")
}
expErr := fmt.Sprintf("unable to compile the schema: remote reference loading disabled: %s/_definitions.json", ts.URL)
if exp, act := expErr, err.Error(); act != exp {
t.Errorf("expected message %q, got %q", exp, act)
}
})
t.Run("all remote refs enabled", func(t *testing.T) {
newtype, err := loadSchema(sch, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if newtype == nil {
t.Fatalf("parseSchema returned nil type")
}
if newtype.String() != "object<apiVersion: string, kind: string, metadata: object<annotations: object[any: any], clusterName: string, creationTimestamp: string, deletionGracePeriodSeconds: number, deletionTimestamp: string, finalizers: array[string], generateName: string, generation: number, initializers: object<pending: array[object<name: string>], result: object<apiVersion: string, code: number, details: object<causes: array[object<field: string, message: string, reason: string>], group: string, kind: string, name: string, retryAfterSeconds: number, uid: string>, kind: string, message: string, metadata: object<continue: string, resourceVersion: string, selfLink: string>, reason: string, status: string>>, labels: object[any: any], managedFields: array[object<apiVersion: string, fields: object[any: any], manager: string, operation: string, time: string>], name: string, namespace: string, ownerReferences: array[object<apiVersion: string, blockOwnerDeletion: boolean, controller: boolean, kind: string, name: string, uid: string>], resourceVersion: string, selfLink: string, uid: string>>" {
t.Fatalf("parseSchema returned an incorrect type: %s", newtype.String())
}
})
t.Run("desired remote ref selectively enabled", func(t *testing.T) {
newtype, err := loadSchema(sch, []string{"127.0.0.1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if newtype == nil {
t.Fatalf("parseSchema returned nil type")
}
if newtype.String() != "object<apiVersion: string, kind: string, metadata: object<annotations: object[any: any], clusterName: string, creationTimestamp: string, deletionGracePeriodSeconds: number, deletionTimestamp: string, finalizers: array[string], generateName: string, generation: number, initializers: object<pending: array[object<name: string>], result: object<apiVersion: string, code: number, details: object<causes: array[object<field: string, message: string, reason: string>], group: string, kind: string, name: string, retryAfterSeconds: number, uid: string>, kind: string, message: string, metadata: object<continue: string, resourceVersion: string, selfLink: string>, reason: string, status: string>>, labels: object[any: any], managedFields: array[object<apiVersion: string, fields: object[any: any], manager: string, operation: string, time: string>], name: string, namespace: string, ownerReferences: array[object<apiVersion: string, blockOwnerDeletion: boolean, controller: boolean, kind: string, name: string, uid: string>], resourceVersion: string, selfLink: string, uid: string>>" {
t.Fatalf("parseSchema returned an incorrect type: %s", newtype.String())
}
})
t.Run("different remote ref selectively enabled", func(t *testing.T) {
_, err := loadSchema(sch, []string{"foo"})
if err == nil {
t.Fatal("expected error, got nil")
}
expErr := fmt.Sprintf("unable to compile the schema: remote reference loading disabled: %s/_definitions.json", ts.URL)
if exp, act := expErr, err.Error(); act != exp {
t.Errorf("expected message %q, got %q", exp, act)
}
})
}
func TestSetTypesWithPodSchema(t *testing.T) {
var sch any
ts := kubeSchemaServer(t)
t.Cleanup(ts.Close)
podSchemaReplaced := strings.ReplaceAll(podSchema, "https://kubernetesjsonschema.dev/v1.14.0/", ts.URL+"/")
err := util.Unmarshal([]byte(podSchemaReplaced), &sch)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
newtype, err := loadSchema(sch, nil)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if newtype == nil {
t.Fatalf("parseSchema returned nil type")
}
if newtype.String() == "object<apiVersion: string, kind: string, metadata: any, spec: any, status: any>" {
t.Fatalf("parseSchema returned an incorrect type: %s", newtype.String())
}
}
func TestAllOfSchemas(t *testing.T) {
// Test 1: object schema
objectSchemaStaticProps := []*types.StaticProperty{
{Key: "AddressLine1", Value: types.S},
{Key: "AddressLine2", Value: types.S},
{Key: "City", Value: types.S},
{Key: "State", Value: types.S},
{Key: "ZipCode", Value: types.S},
{Key: "County", Value: types.S},
{Key: "PostCode", Value: types.S},
}
objectSchemaExpectedType := types.NewObject(objectSchemaStaticProps, nil)
// Test 2: array schema
arrayExpectedType := types.NewArray(nil, types.N)
// Test 3: parent variation
parentVariationStaticProps := []*types.StaticProperty{
{Key: "State", Value: types.S},
{Key: "ZipCode", Value: types.S},
{Key: "County", Value: types.S},
{Key: "PostCode", Value: types.S},
}
parentVariationExpectedType := types.NewObject(parentVariationStaticProps, nil)
// Test 4: empty schema with allOf
emptyExpectedType := types.A
// Tests 5 & 6: schema with array of arrays, object and array as siblings
expectedError := errors.New("unable to merge these schemas")
// Test 7: array of objects
arrayOfObjectsStaticProps := []*types.StaticProperty{
{Key: "State", Value: types.S},
{Key: "ZipCode", Value: types.S},
{Key: "County", Value: types.S},
{Key: "PostCode", Value: types.S},
{Key: "Street", Value: types.S},
{Key: "House", Value: types.S},
}
innerType := types.NewObject(arrayOfObjectsStaticProps, nil)
arrayOfObjectsExpectedType := types.NewArray(nil, innerType)
// Tests 8 & 9: allOf schema with type not specified
objectMissingStaticProps := []*types.StaticProperty{
{Key: "AddressLine", Value: types.S},
{Key: "State", Value: types.S},
{Key: "ZipCode", Value: types.S},
{Key: "County", Value: types.S},
{Key: "PostCode", Value: types.N},
}
objectMissingExpectedType := types.NewObject(objectMissingStaticProps, nil)
arrayMissingExpectedType := types.NewArray([]types.Type{types.N, types.N}, nil)
// Tests 10 & 11: allOf schema with array that contains different types (with and without error)
arrayDifTypesExpectedType := types.NewArray([]types.Type{types.S, types.N}, nil)
// Test 12: array inside of object
arrayInObjectstaticProps := []*types.StaticProperty{
{Key: "age", Value: types.N},
{Key: "name", Value: types.S},
{Key: "personality", Value: types.S},
{Key: "nickname", Value: types.S},
}
innerObjectsType := types.NewObject(arrayInObjectstaticProps, nil)
arrayInObjectInnerType := types.NewArray(nil, innerObjectsType)
arrayInObjectExpectedType := types.NewObject([]*types.StaticProperty{
types.NewStaticProperty("familyMembers", arrayInObjectInnerType)}, nil)
// Test 13: allOf inside core schema
coreStaticProps := []*types.StaticProperty{
{Key: "accessMe", Value: types.S},
{Key: "accessYou", Value: types.S},
}
insideType := types.NewObject(coreStaticProps, nil)
outerType := []*types.StaticProperty{
{Key: "RandomInfo", Value: insideType},
{Key: "AddressLine", Value: types.S},
}
coreSchemaExpectedType := types.NewObject(outerType, nil)
// Test 14-17: other types besides array and object
// Test 18: array with uneven numbers of items children to merge
expectedUnevenArrayType := types.NewArray([]types.Type{types.N, types.N, types.S}, nil)
tests := []struct {
note string
schema string
expectedType types.Type
expectedError error
}{
{
note: "allOf with mergeable Object types in schema",
schema: allOfObjectSchema,
expectedType: objectSchemaExpectedType,
expectedError: nil,
},
{
note: "allOf with mergeable Array types in schema",
schema: allOfArraySchema,
expectedType: arrayExpectedType,
expectedError: nil,
},
{
note: "allOf without a parent schema",
schema: allOfSchemaParentVariation,
expectedType: parentVariationExpectedType,
expectedError: nil,
},
{
note: "allOf with empty schema",
schema: emptySchema,
expectedType: emptyExpectedType,
expectedError: nil,
},
{
note: "allOf schema with unmergeable Array of Arrays",
schema: allOfArrayOfArrays,
expectedType: nil,
expectedError: expectedError,
},
{
note: "allOf with mergeable Array of Object types in schema",
schema: allOfArrayOfObjects,
expectedType: arrayOfObjectsExpectedType,
expectedError: nil,
},
{
note: "allOf schema with Array and Object types as siblings",
schema: allOfObjectAndArray,
expectedType: nil,
expectedError: expectedError,
},
{
note: "allOf with mergeable Object types in schema with type declaration missing",
schema: allOfObjectMissing,
expectedType: objectMissingExpectedType,
expectedError: nil,
},
{
note: "allOf with mergeable Array types in schema with type declaration missing",
schema: allOfArrayMissing,
expectedType: arrayMissingExpectedType,
expectedError: nil,
},
{
note: "allOf schema with an Array that contains different mergeable types",
schema: allOfArrayDifTypes,
expectedType: arrayDifTypesExpectedType,
expectedError: nil,
},
{
note: "allOf schema with Array type that contains different unmergeable types",
schema: allOfArrayDifTypesWithError,
expectedType: nil,
expectedError: expectedError,
},
{
note: "allOf with mergeable Object containing Array types in schema",
schema: allOfArrayInsideObject,
expectedType: arrayInObjectExpectedType,
expectedError: nil,
},
{
note: "allOf with mergeable types inside of core schema",
schema: allOfInsideCoreSchema,
expectedType: coreSchemaExpectedType,
expectedError: nil,
},
{
note: "allOf with mergeable String types in schema",
schema: allOfStringSchema,
expectedType: types.S,
expectedError: nil,
},
{
note: "allOf with mergeable Integer types in schema",
schema: allOfIntegerSchema,
expectedType: types.N,
expectedError: nil,
},
{
note: "allOf with mergeable Boolean types in schema",
schema: allOfBooleanSchema,
expectedType: types.B,
expectedError: nil,
},
{
note: "allOf schema with different unmergeable types",
schema: allOfTypeErrorSchema,
expectedType: nil,
expectedError: expectedError,
},
{
note: "allOf schema with unmergeable types String and Boolean",
schema: allOfStringSchemaWithError,
expectedType: nil,
expectedError: expectedError,
},
{
note: "allOf unmergeable schema with different parent and items types",
schema: allOfSchemaWithParentError,
expectedType: nil,
expectedError: expectedError,
},
{
note: "allOf schema of Array type with uneven numbers of items to merge",
schema: allOfSchemaWithUnevenArray,
expectedType: expectedUnevenArrayType,
expectedError: nil,
},
{
note: "allOf with single Object type through $ref",
schema: allOfRef,
expectedType: types.NewObject([]*types.StaticProperty{
{Key: "v", Value: types.A},
}, nil),
expectedError: nil,
},
{
note: "allOf with $ref issue #6523: nested $ref in allOf resolved to correct type",
schema: iss6523,
expectedType: types.NewObject([]*types.StaticProperty{
{Key: "action", Value: types.S},
{Key: "context", Value: types.NewObject([]*types.StaticProperty{
{Key: "groups", Value: types.S},
{Key: "user", Value: types.S},
}, nil)},
}, nil),
expectedError: nil,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
testParseSchema(t, tc.schema, tc.expectedType, tc.expectedError)
})
}
}
func TestParseSchemaUntypedField(t *testing.T) {
// Expected type is: object<foo: any>
staticProps := make([]*types.StaticProperty, 0, 1)
staticProps = append(staticProps, &types.StaticProperty{Key: "foo", Value: types.A})
expectedType := types.NewObject(staticProps, nil)
testParseSchema(t, untypedFieldObjectSchema, expectedType, nil)
}
func TestParseSchemaNoChildren(t *testing.T) {
// Expected type is: object[any: any]
expectedType := types.NewObject(nil, &types.DynamicProperty{Key: types.A, Value: types.A})
testParseSchema(t, noChildrenObjectSchema, expectedType, nil)
}
func TestParseSchemaArrayNoItems(t *testing.T) {
// Expected type is: object<b: array[any]>
staticProps := make([]*types.StaticProperty, 0, 1)
staticProps = append(staticProps, &types.StaticProperty{Key: "b", Value: types.NewArray(nil, types.A)})
expectedType := types.NewObject(staticProps, nil)
testParseSchema(t, arrayNoItemsSchema, expectedType, nil)
}
func TestParseSchemaBooleanField(t *testing.T) {
// Expected type is: object<a: boolean>
staticProps := make([]*types.StaticProperty, 0, 1)
staticProps = append(staticProps, &types.StaticProperty{Key: "a", Value: types.B})
expectedType := types.NewObject(staticProps, nil)
testParseSchema(t, booleanSchema, expectedType, nil)
}
func TestParseSchemaBasics(t *testing.T) {
tests := []struct {
note string
schema string
exp types.Type
}{
{
note: "number",
schema: `{"type": "number"}`,
exp: types.N,
},
{
note: "array of objects",
schema: `{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"value": {"type": "number"}
}
}
}`,
exp: types.NewArray(nil, types.NewObject([]*types.StaticProperty{
types.NewStaticProperty("id", types.S),
types.NewStaticProperty("value", types.N),
}, nil)),
},
{
note: "static array items",
schema: `{
"type": "array",
"items": [
{"type": "string"},
{"type": "number"}
]
}`,
exp: types.NewArray([]types.Type{
types.S,
types.N,
}, nil),
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
testParseSchema(t, tc.schema, tc.exp, nil)
})
}
}
func TestCompileSchemaEmptySchema(t *testing.T) {
schema := ""
var sch any
err := util.Unmarshal([]byte(schema), &sch)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
jsonSchema, _ := compileSchema(sch, []string{})
if jsonSchema != nil {
t.Fatalf("Incorrect return from parseSchema with an empty schema")
}
}
func TestParseSchemaWithSchemaBadSchema(t *testing.T) {
var sch any
err := util.Unmarshal([]byte(objectSchema), &sch)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
jsonSchema, err := compileSchema(sch, []string{})
if err != nil {
t.Fatalf("Unable to compile schema: %v", err)
}
newtype, err := newSchemaParser().parseSchema(jsonSchema) // Did not pass the subschema
if err == nil {
t.Fatalf("Expected parseSchema() = error, got nil")
}
if newtype != nil {
t.Fatalf("Incorrect return from parseSchema with a bad schema")
}
}
func TestAnyOfSchema(t *testing.T) {
// Test 1 & 3: anyOf extends the core schema
extendCoreStaticPropsExp1 := []*types.StaticProperty{types.NewStaticProperty("AddressLine", types.S)}
extendCoreStaticPropsExp2 := []*types.StaticProperty{
types.NewStaticProperty("State", types.S),
types.NewStaticProperty("ZipCode", types.S)}
extendCoreStaticPropsExp3 := []*types.StaticProperty{
types.NewStaticProperty("County", types.S),
types.NewStaticProperty("PostCode", types.N)}
extendCoreSchemaTypeExp := types.Or(types.NewObject(extendCoreStaticPropsExp3, nil),
types.Or(
types.NewObject(extendCoreStaticPropsExp1, nil),
types.NewObject(extendCoreStaticPropsExp2, nil)))
// Test 2: anyOf is inside the core schema
insideCoreObjType := types.NewObject([]*types.StaticProperty{
types.NewStaticProperty("accessMe", types.S)}, nil)
insideCoreAnyType := types.Or(insideCoreObjType, types.N)
insideCoreStaticProps := []*types.StaticProperty{
types.NewStaticProperty("AddressLine", types.S),
types.NewStaticProperty("RandomInfo", insideCoreAnyType)}
insideCoreTypeExp := types.NewObject(insideCoreStaticProps, nil)
// Test 4: anyOf in an array
arrayObjType1 := types.NewObject([]*types.StaticProperty{
types.NewStaticProperty("age", types.N),
types.NewStaticProperty("name", types.S)}, nil)
arrayObjType2 := types.NewObject([]*types.StaticProperty{
types.NewStaticProperty("personality", types.S),
types.NewStaticProperty("nickname", types.S)}, nil)
arrayOrType := types.Or(arrayObjType1, arrayObjType2)
arrayArrayType := types.NewArray(nil, arrayOrType)
arrayObjtype := types.NewObject([]*types.StaticProperty{
types.NewStaticProperty("familyMembers", arrayArrayType)}, nil)
// Test 5: anyOf array has items but not specified
arrayMissing1 := types.NewArray([]types.Type{
types.N, types.S}, nil)
arrayMissing2 := types.NewArray([]types.Type{types.N}, nil)
arrayMissingType := types.Or(arrayMissing1, arrayMissing2)
// Test 6: anyOf Parent variation schema
anyOfParentVar1 := types.NewObject([]*types.StaticProperty{
types.NewStaticProperty("State", types.S),
types.NewStaticProperty("ZipCode", types.S)}, nil)
anyOfParentVar2 := types.NewObject([]*types.StaticProperty{
types.NewStaticProperty("County", types.S),
types.NewStaticProperty("PostCode", types.S)}, nil)
anyOfParentVarType := types.Or(anyOfParentVar1, anyOfParentVar2)
tests := []struct {
note string
schema string
expected types.Type
}{
{
note: "anyOf extend core schema",
schema: anyOfExtendCoreSchema,
expected: extendCoreSchemaTypeExp,
},
{
note: "anyOf inside core schema",
schema: anyOfInsideCoreSchema,
expected: insideCoreTypeExp,
},
{
note: "anyOf object missing type",
schema: anyOfObjectMissing,
expected: extendCoreSchemaTypeExp,
},
{
note: "anyOf of an array",
schema: anyOfArraySchema,
expected: arrayObjtype,
},
{
note: "anyOf array missing type",
schema: anyOfArrayMissing,
expected: arrayMissingType,
},
{
note: "anyOf as parent",
schema: anyOfSchemaParentVariation,
expected: anyOfParentVarType,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
testParseSchema(t, tc.schema, tc.expected, nil)
})
}
}
func kubeSchemaServer(t *testing.T) *httptest.Server {
t.Helper()
bs, err := os.ReadFile("testdata/_definitions.json")
if err != nil {
t.Fatal(err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write(bs)
if err != nil {
panic(err)
}
}))
return ts
}
func TestCompilerCheckTypesWithSchema(t *testing.T) {
c := NewCompiler()
var schema any
err := util.Unmarshal([]byte(objectSchema), &schema)
if err != nil {
t.Fatal("Unexpected error:", err)
}
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, schema)
c.WithSchemas(schemaSet)
compileStages(c, StageCheckTypes)
assertNotFailed(t, c)
}
func TestCompilerCheckTypesWithRegexPatternInSchema(t *testing.T) {
c := NewCompiler()
var schema any
// Negative lookahead is not supported in the Go regex dialect, but this is still a valid
// JSON schema. Since we don't rely on the "pattern" attribute for type checking, ensure
// that this still works (by being ignored)
err := util.Unmarshal([]byte(`{
"properties": {
"name": {
"pattern": "^(?!testing:.*)[a-z]+$",
"type": "string"
}
}
}`), &schema)
if err != nil {
t.Fatal("Unexpected error:", err)
}
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, schema)
c.WithSchemas(schemaSet)
compileStages(c, StageCheckTypes)
assertNotFailed(t, c)
}
func TestCompilerCheckTypesWithAllOfSchema(t *testing.T) {
tests := []struct {
note string
schema string
expectedError error
}{
{
note: "allOf with mergeable Object types in schema",
schema: allOfObjectSchema,
expectedError: nil,
},
{
note: "allOf with mergeable Array types in schema",
schema: allOfArraySchema,
expectedError: nil,
},
{
note: "allOf without a parent schema",
schema: allOfSchemaParentVariation,
expectedError: nil,
},
{
note: "allOf with empty schema",
schema: emptySchema,
expectedError: nil,
},
{
note: "allOf with mergeable Array of Object types in schema",
schema: allOfArrayOfObjects,
expectedError: nil,
},
{
note: "allOf with mergeable Object types in schema with type declaration missing",
schema: allOfObjectMissing,
expectedError: nil,
},
{
note: "allOf with Array of mergeable different types in schema",
schema: allOfArrayDifTypes,
expectedError: nil,
},
{
note: "allOf with mergeable Object containing Array types in schema",
schema: allOfArrayInsideObject,
expectedError: nil,
},
{
note: "allOf with mergeable Array types in schema with type declaration missing",
schema: allOfArrayMissing,
expectedError: nil,
},
{
note: "allOf with mergeable types inside of core schema",
schema: allOfInsideCoreSchema,
expectedError: nil,
},
{
note: "allOf with mergeable String types in schema",
schema: allOfStringSchema,
expectedError: nil,
},
{
note: "allOf with mergeable Integer types in schema",
schema: allOfIntegerSchema,
expectedError: nil,
},
{
note: "allOf with mergeable Boolean types in schema",
schema: allOfBooleanSchema,
expectedError: nil,
},
{
note: "allOf with mergeable Array types with uneven numbers of items",
schema: allOfSchemaWithUnevenArray,
expectedError: nil,
},
{
note: "allOf with single Object type through ref",
schema: allOfRef,
expectedError: nil,
},
{
note: "allOf with mergeable Object types through ref",
schema: allOfObjectRef,
expectedError: nil,
},
{
note: "allOf with anyOf containing $ref types",
schema: allOfAnyOfRef,
expectedError: nil,
},
{
note: "allOf with $ref issue #6523",
schema: iss6523,
expectedError: nil,
},
{
note: "allOf $ref unmergable",
schema: allOfRefUnmergable,
expectedError: errors.New("unable to merge these schemas: type mismatch: string and integer"),
},
{
note: "allOf schema with unmergeable Array of Arrays",
schema: allOfArrayOfArrays,
expectedError: errors.New("unable to merge these schemas"),
},
{
note: "allOf schema with Array and Object types as siblings",
schema: allOfObjectAndArray,
expectedError: errors.New("unable to merge these schemas"),
},
{
note: "allOf schema with Array type that contains different unmergeable types",
schema: allOfArrayDifTypesWithError,
expectedError: errors.New("unable to merge these schemas"),
},
{
note: "allOf schema with different unmergeable types",
schema: allOfTypeErrorSchema,
expectedError: errors.New("unable to merge these schemas"),
},
{
note: "allOf unmergeable schema with different parent and items types",
schema: allOfSchemaWithParentError,
expectedError: errors.New("unable to merge these schemas"),
},
{
note: "allOf schema of Array type with uneven numbers of items to merge",
schema: allOfSchemaWithUnevenArray,
expectedError: nil,
},
{
note: "allOf schema with unmergeable types String and Boolean",
schema: allOfStringSchemaWithError,
expectedError: errors.New("unable to merge these schemas"),
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
c := NewCompiler()
var schema any
err := util.Unmarshal([]byte(tc.schema), &schema)
if err != nil {
t.Fatal("Unexpected error:", err)
}
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, schema)
c.WithSchemas(schemaSet)
compileStages(c, StageCheckTypes)
if tc.expectedError != nil {
if !strings.Contains(c.Errors.Error(), tc.expectedError.Error()) {
t.Fatal("Unexpected error:", c.Errors)
}
} else {
assertNotFailed(t, c)
}
})
}
}
func TestWithSchema(t *testing.T) {
c := NewCompiler()
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, objectSchema)
c.WithSchemas(schemaSet)
if c.schemaSet == nil {
t.Fatalf("WithSchema did not set the schema correctly in the compiler")
}
}
func TestAnyOfObjectSchema1(t *testing.T) {
c := NewCompiler()
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, anyOfExtendCoreSchema)
c.WithSchemas(schemaSet)
if c.schemaSet == nil {
t.Fatalf("Did not correctly compile an object type schema with anyOf outside core schema")
}
}
func TestAnyOfObjectSchema2(t *testing.T) {
c := NewCompiler()
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, anyOfInsideCoreSchema)
c.WithSchemas(schemaSet)
if c.schemaSet == nil {
t.Fatalf("Did not correctly compile an object type schema with anyOf inside core schema")
}
}
func TestAnyOfArraySchema(t *testing.T) {
c := NewCompiler()
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, anyOfArraySchema)
c.WithSchemas(schemaSet)
if c.schemaSet == nil {
t.Fatalf("Did not correctly compile an array type schema with anyOf")
}
}
func TestAnyOfObjectMissing(t *testing.T) {
c := NewCompiler()
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, anyOfObjectMissing)
c.WithSchemas(schemaSet)
if c.schemaSet == nil {
t.Fatalf("Did not correctly compile an object type schema with anyOf where one of the props did not explicitly claim type")
}
}
func TestAnyOfArrayMissing(t *testing.T) {
c := NewCompiler()
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, anyOfArrayMissing)
c.WithSchemas(schemaSet)
if c.schemaSet == nil {
t.Fatalf("Did not correctly compile an array type schema with anyOf where items are inside anyOf")
}
}
func TestRecursiveSchema(t *testing.T) {
c := NewCompiler()
schemaSet := NewSchemaSet()
schemaSet.Put(SchemaRootRef, recursiveElements)
c.WithSchemas(schemaSet)
if c.schemaSet == nil {
t.Fatalf("Did not correctly compile an object schema with recursive elements")
}
}
const objectSchema = `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"required": [
"foo",
"b"
],
"properties": {
"foo": {
"$id": "#/properties/foo",
"type": "string",
"title": "The foo schema",
"description": "An explanation about the purpose of this instance."
},
"b": {
"$id": "#/properties/b",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"additionalItems": false,
"items": {
"$id": "#/properties/b/items",
"type": "object",
"title": "The items schema",
"description": "An explanation about the purpose of this instance.",
"required": [
"a",
"b",
"c"
],
"properties": {
"a": {
"$id": "#/properties/b/items/properties/a",
"type": "integer",
"title": "The a schema",
"description": "An explanation about the purpose of this instance."
},
"b": {
"$id": "#/properties/b/items/properties/b",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"additionalItems": false,
"items": {
"$id": "#/properties/b/items/properties/b/items",
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
}
},
"c": {
"$id": "#/properties/b/items/properties/c",
"type": "null",
"title": "The c schema",
"description": "An explanation about the purpose of this instance."
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}`
const arrayNoItemsSchema = `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"required": [
"b"
],
"properties": {
"b": {
"$id": "#/properties/b",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"additionalItems": true
}
},
"additionalProperties": false
}`
const noChildrenObjectSchema = `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"additionalProperties": true
}`
const untypedFieldObjectSchema = `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"required": [
"foo"
],
"properties": {
"foo": {
"$id": "#/properties/foo"
}
},
"additionalProperties": false
}`
const booleanSchema = `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"required": [
"a"
],
"properties": {
"a": {
"$id": "#/properties/foo",
"type": "boolean",
"title": "The foo schema",
"description": "An explanation about the purpose of this instance."
}
},
"additionalProperties": false
}`
const refSchema = `
{
"description": "Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts.",
"type": "object",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
"type": [
"string",
"null"
]
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
"type": [
"string",
"null"
],
"enum": [
"Pod"
]
},
"metadata": {
"$ref": "https://kubernetesjsonschema.dev/v1.14.0/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
"description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata"
}
}
}
`
const podSchema = `
{
"description": "Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts.",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
"type": [
"string",
"null"
]
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
"type": [
"string",
"null"
],
"enum": [
"Pod"
]
},
"metadata": {
"$ref": "https://kubernetesjsonschema.dev/v1.14.0/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
"description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata"
},
"spec": {
"$ref": "https://kubernetesjsonschema.dev/v1.14.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodSpec",
"description": "Specification of the desired behavior of the pod. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status"
},
"status": {
"$ref": "https://kubernetesjsonschema.dev/v1.14.0/_definitions.json#/definitions/io.k8s.api.core.v1.PodStatus",
"description": "Most recently observed status of the pod. This data may not be up to date. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status"
}
},
"type": "object",
"x-kubernetes-group-version-kind": [
{
"group": "",
"kind": "Pod",
"version": "v1"
}
],
"$schema": "http://json-schema.org/schema#"
}`
const anyOfArraySchema = `{
"type": "object",
"properties": {
"familyMembers": {
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"age": { "type": "integer" },
"name": {"type": "string"}
}
},{
"type": "object",
"properties": {
"personality": { "type": "string" },
"nickname": { "type": "string" }
}
}
]
}
}
}
}`
const anyOfExtendCoreSchema = `{
"type": "object",
"properties": {
"AddressLine": { "type": "string" }
},
"anyOf": [
{
"type": "object",
"properties": {
"State": { "type": "string" },
"ZipCode": { "type": "string" }
}
},
{
"type": "object",
"properties": {
"County": { "type": "string" },
"PostCode": { "type": "integer" }
}
}
]
}`
const allOfObjectSchema = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "My schema",
"properties": {
"AddressLine1": { "type": "string" },
"AddressLine2": { "type": "string" },
"City": { "type": "string" }
},
"allOf": [
{
"type": "object",
"properties": {
"State": { "type": "string" },
"ZipCode": { "type": "string" }
},
},
{
"type": "object",
"properties": {
"County": { "type": "string" },
"PostCode": { "type": "string" }
},
}
]
}`
const allOfArraySchema = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
},
"allOf": [
{
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
}
},
{
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
}
}
]
}`
const allOfSchemaParentVariation = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"allOf": [
{
"type": "object",
"properties": {
"State": { "type": "string" },
"ZipCode": { "type": "string" }
},
},
{
"type": "object",
"properties": {
"County": { "type": "string" },
"PostCode": { "type": "string" }
},
}
]
}`
const emptySchema = `{
"allof" : []
}`
const allOfArrayOfArrays = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "array",
"title": "The items schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
}
},
"allOf": [{
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "array",
"title": "The items schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
}
}
},
{
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
}
}
]
}`
const anyOfInsideCoreSchema = ` {
"type": "object",
"properties": {
"AddressLine": { "type": "string" },
"RandomInfo": {
"anyOf": [
{ "type": "object",
"properties": {
"accessMe": {"type": "string"}
}
},
{ "type": "number", "minimum": 0 }
]
}
}
}`
const anyOfObjectMissing = `{
"type": "object",
"properties": {
"AddressLine": { "type": "string" }
},
"anyOf": [
{
"type": "object",
"properties": {
"State": { "type": "string" },
"ZipCode": { "type": "string" }
}
},
{
"properties": {
"County": { "type": "string" },
"PostCode": { "type": "integer" }
}
}
]
}`
const allOfArrayOfObjects = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "object",
"title": "The items schema",
"description": "An explanation about the purpose of this instance.",
"properties": {
"State": {
"type": "string"
},
"ZipCode": {
"type": "string"
}
},
"allOf": [{
"type": "object",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"properties": {
"County": {
"type": "string"
},
"PostCode": {
"type": "string"
}
}
},
{
"type": "object",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"properties": {
"Street": {
"type": "string"
},
"House": {
"type": "string"
}
}
}
]
}
}`
const allOfObjectAndArray = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "My schema",
"properties": {
"AddressLine1": {
"type": "string"
},
"AddressLine2": {
"type": "string"
},
"City": {
"type": "string"
}
},
"allOf": [{
"type": "object",
"properties": {
"State": {
"type": "string"
},
"ZipCode": {
"type": "string"
}
}
},
{
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"items": {
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
}
}
]
}`
const allOfObjectMissing = `{
"type": "object",
"properties": {
"AddressLine": { "type": "string" }
},
"allOf": [
{
"type": "object",
"properties": {
"State": { "type": "string" },
"ZipCode": { "type": "string" }
}
},
{
"properties": {
"County": { "type": "string" },
"PostCode": { "type": "integer" }
}
}
]
}`
const allOfArrayDifTypes = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"allOf": [{
"type": "array",
"items": [{
"type": "string"
},
{
"type": "integer"
}
]
},
{
"type": "array",
"items": [{
"type": "string"
},
{
"type": "integer"
}
]
}
]
}`
const allOfArrayInsideObject = `{
"type": "object",
"properties": {
"familyMembers": {
"type": "array",
"items": {
"allOf": [{
"type": "object",
"properties": {
"age": {
"type": "integer"
},
"name": {
"type": "string"
}
}
}, {
"type": "object",
"properties": {
"personality": {
"type": "string"
},
"nickname": {
"type": "string"
}
}
}]
}
}
}
}`
const anyOfArrayMissing = `{
"type": "array",
"anyOf": [
{
"items": [
{"type": "number"},
{"type": "string"}]
},
{ "items": [
{"type": "integer"}]
}
]
}`
const allOfArrayMissing = `{
"type": "array",
"allOf": [{
"items": [{
"type": "integer"
},
{
"type": "integer"
}
]
},
{
"items": [{
"type": "integer"
}]
}
]
}`
const allOfRef = `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [{"$ref": "#/$defs/d"}],
"$defs": {
"d": {
"type": "object",
"properties": {
"v": {"const": 1}
},
"additionalProperties": false
}
}
}`
const allOfObjectRef = `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"type": "object",
"properties": {
"v": {"integer"}
}
},
{"$ref": "#/$defs/d"}
],
"$defs": {
"d": {
"type": "object",
"properties": {
"v": {"integer"}
}
}
}
}`
const allOfAnyOfRef = `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"type": "object",
"properties": {
"v": {"enum": [1, 2]}
}
},
{"anyOf": [{"$ref": "#/$defs/d1"}, {"$ref": "#/$defs/d2"}]}
],
"$defs": {
"d1": {
"type": "object",
"properties": {
"v": {"const": 1},
"k": {"type": "integer"}
},
"additionalProperties": false
},
"d2": {
"type": "object",
"properties": {
"v": {"const": 2},
"j": {"type": "string"}
},
"additionalProperties": false
}
}
}`
const iss6523 = `{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://jsonschema.dev/schemas/opa-input",
"$defs": {
"mixins/action.schema.json": {
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "mixins/action.schema.json",
"type": "string"
},
"mixins/context.schema.json": {
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "mixins/context.schema.json",
"type": "object",
"properties": {
"user": {
"type": "string"
}
},
"required": [
"user"
]
},
"mixins/special-context.schema.json": {
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "mixins/special-context.schema.json",
"type": "object",
"properties": {
"groups": {
"type": "string"
}
},
"allOf": [
{
"$ref": "https://jsonschema.dev/schemas/mixins/context.schema.json"
}
],
"required": [
"groups"
]
}
},
"type": "object",
"properties": {
"action": {
"$ref": "https://jsonschema.dev/schemas/mixins/action.schema.json"
},
"context": {
"$ref": "https://jsonschema.dev/schemas/mixins/special-context.schema.json"
}
},
"required": [
"action",
"context"
]
}`
const allOfRefUnmergable = `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://jsonschema.dev/schemas/opa-input",
"$defs": {
"d1": {
"type": "string"
},
"d2": {
"type": "integer",
}
},
"allOf": [{"$ref": "#/$defs/d1"}, {"$ref": "#/$defs/d2"}]
}`
const anyOfSchemaParentVariation = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"anyOf": [
{
"type": "object",
"properties": {
"State": { "type": "string" },
"ZipCode": { "type": "string" }
},
},
{
"type": "object",
"properties": {
"County": { "type": "string" },
"PostCode": { "type": "string" }
},
}
]
}
}`
const allOfInsideCoreSchema = `{
"type": "object",
"properties": {
"AddressLine": { "type": "string" },
"RandomInfo": {
"allOf": [
{ "type": "object",
"properties": {
"accessMe": {"type": "string"}
}
},
{ "type": "object",
"properties": {
"accessYou": {"type": "string"}
}}
]
}
}
}`
const allOfArrayDifTypesWithError = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"allOf": [{
"type": "array",
"items": [{
"type": "string"
},
{
"type": "integer"
}
]
},
{
"type": "array",
"items": [{
"type": "boolean"
},
{
"type": "integer"
}
]
}
]
}`
const allOfStringSchema = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "string",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"allOf": [{
"type": "string",
},
{
"type": "string",
}
]
}`
const allOfIntegerSchema = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "integer",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"allOf": [{
"type": "integer",
},
{
"type": "integer",
}
]
}`
const allOfBooleanSchema = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "boolean",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"allOf": [{
"type": "boolean",
},
{
"type": "boolean",
}
]
}`
const allOfTypeErrorSchema = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "string",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"allOf": [{
"type": "string",
},
{
"type": "integer",
}
]
}`
const allOfStringSchemaWithError = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "string",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"allOf": [{
"type": "string",
},
{
"type": "string",
},
{
"type": "boolean",
}
]
}`
const allOfSchemaWithParentError = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "string",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"allOf": [{
"type": "integer",
},
{
"type": "integer",
}
]
}`
const allOfSchemaWithUnevenArray = `{
"type": "array",
"allOf": [{
"items": [{
"type": "integer"
},
{
"type": "integer"
}
]
},
{
"items": [{
"type": "integer"
},
{
"type": "integer"
},
{
"type": "string"
}]
}
]
}`
const recursiveElements = `{
"type": "object",
"properties": {
"Something": {
"$ref": "#/$defs/X"
}
},
"$defs": {
"X": {
"type": "object",
"properties": {
"Y": {
"$ref": "#/$defs/Y"
}
}
},
"Y": {
"type": "object",
"properties": {
"X": {
"$ref": "#/$defs/X"
}
}
}
}
}
`