Skip to content

Commit 156409b

Browse files
committed
Address issue #753
Handles discriminator validation on schemas.
1 parent 58d1522 commit 156409b

File tree

2 files changed

+248
-0
lines changed

2 files changed

+248
-0
lines changed

functions/openapi/schema_type.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ func (st SchemaTypeCheck) RunRule(_ []*yaml.Node, context model.RuleFunctionCont
103103
// validate enum and const are not conflicting
104104
enumConstErrs := st.validateEnumConst(schema, &context)
105105
results = append(results, enumConstErrs...)
106+
107+
// validate discriminator property existence
108+
discriminatorErrs := st.validateDiscriminator(schema, &context)
109+
results = append(results, discriminatorErrs...)
106110
}
107111

108112
return results
@@ -842,3 +846,45 @@ func (st SchemaTypeCheck) isConstValueValidForType(value interface{}, schemaType
842846
}
843847
return false
844848
}
849+
850+
// validateDiscriminator checks that discriminator.propertyName exists in schema properties or polymorphic compositions.
851+
// note: discriminator properties are not required to be in the required array per OpenAPI spec.
852+
func (st SchemaTypeCheck) validateDiscriminator(schema *v3.Schema, context *model.RuleFunctionContext) []model.RuleFunctionResult {
853+
var results []model.RuleFunctionResult
854+
855+
if schema.Discriminator == nil {
856+
return results
857+
}
858+
859+
discriminator := schema.Discriminator
860+
propertyName := discriminator.Value.PropertyName
861+
862+
// propertyName is required per OpenAPI 3.x spec
863+
if propertyName == "" {
864+
result := st.buildResult(
865+
"discriminator object is missing required `propertyName` field",
866+
schema.GenerateJSONPath(), "discriminator", -1,
867+
schema, discriminator.KeyNode, context)
868+
results = append(results, result)
869+
return results
870+
}
871+
872+
propertyExists := false
873+
if schema.Value.Properties != nil && schema.Value.Properties.GetOrZero(propertyName) != nil {
874+
propertyExists = true
875+
}
876+
877+
if !propertyExists {
878+
propertyExists = st.checkPolymorphicProperty(schema, propertyName)
879+
}
880+
881+
if !propertyExists {
882+
result := st.buildResult(
883+
fmt.Sprintf("discriminator property `%s` is not defined in schema properties", propertyName),
884+
schema.GenerateJSONPath(), "discriminator", -1,
885+
schema, discriminator.KeyNode, context)
886+
results = append(results, result)
887+
}
888+
889+
return results
890+
}

functions/openapi/schema_type_test.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3197,3 +3197,205 @@ components:
31973197
}
31983198
}
31993199

3200+
func TestSchemaType_ValidateDiscriminator(t *testing.T) {
3201+
tests := []struct {
3202+
name string
3203+
yaml string
3204+
expectedErrors []struct {
3205+
message string
3206+
path string
3207+
}
3208+
}{
3209+
{
3210+
name: "MissingProperty",
3211+
yaml: `openapi: 3.0.3
3212+
components:
3213+
schemas:
3214+
Pet:
3215+
type: object
3216+
properties:
3217+
name:
3218+
type: string
3219+
age:
3220+
type: integer
3221+
discriminator:
3222+
propertyName: petType
3223+
mapping:
3224+
dog: '#/components/schemas/Dog'
3225+
cat: '#/components/schemas/Cat'`,
3226+
expectedErrors: []struct{ message, path string }{
3227+
{
3228+
message: "discriminator property `petType` is not defined in schema properties",
3229+
path: "$.components.schemas['Pet'].discriminator",
3230+
},
3231+
},
3232+
},
3233+
{
3234+
name: "PropertyExists",
3235+
yaml: `openapi: 3.0.3
3236+
components:
3237+
schemas:
3238+
Pet:
3239+
type: object
3240+
properties:
3241+
petType:
3242+
type: string
3243+
name:
3244+
type: string
3245+
age:
3246+
type: integer
3247+
discriminator:
3248+
propertyName: petType
3249+
mapping:
3250+
dog: '#/components/schemas/Dog'
3251+
cat: '#/components/schemas/Cat'`,
3252+
expectedErrors: []struct{ message, path string }{},
3253+
},
3254+
{
3255+
name: "PropertyInAllOf",
3256+
yaml: `openapi: 3.0.3
3257+
components:
3258+
schemas:
3259+
Pet:
3260+
type: object
3261+
allOf:
3262+
- type: object
3263+
properties:
3264+
petType:
3265+
type: string
3266+
name:
3267+
type: string
3268+
discriminator:
3269+
propertyName: petType
3270+
mapping:
3271+
dog: '#/components/schemas/Dog'
3272+
cat: '#/components/schemas/Cat'`,
3273+
expectedErrors: []struct{ message, path string }{},
3274+
},
3275+
{
3276+
name: "PropertyInOneOf",
3277+
yaml: `openapi: 3.0.3
3278+
components:
3279+
schemas:
3280+
Pet:
3281+
type: object
3282+
oneOf:
3283+
- type: object
3284+
properties:
3285+
petType:
3286+
type: string
3287+
breed:
3288+
type: string
3289+
- type: object
3290+
properties:
3291+
petType:
3292+
type: string
3293+
color:
3294+
type: string
3295+
discriminator:
3296+
propertyName: petType
3297+
mapping:
3298+
dog: '#/components/schemas/Dog'
3299+
cat: '#/components/schemas/Cat'`,
3300+
expectedErrors: []struct{ message, path string }{},
3301+
},
3302+
{
3303+
name: "PropertyInAnyOf",
3304+
yaml: `openapi: 3.0.3
3305+
components:
3306+
schemas:
3307+
Pet:
3308+
type: object
3309+
anyOf:
3310+
- type: object
3311+
properties:
3312+
petType:
3313+
type: string
3314+
name:
3315+
type: string
3316+
discriminator:
3317+
propertyName: petType`,
3318+
expectedErrors: []struct{ message, path string }{},
3319+
},
3320+
{
3321+
name: "EmptyPropertyName",
3322+
yaml: `openapi: 3.0.3
3323+
components:
3324+
schemas:
3325+
Pet:
3326+
type: object
3327+
properties:
3328+
name:
3329+
type: string
3330+
discriminator:
3331+
propertyName: ""`,
3332+
expectedErrors: []struct{ message, path string }{
3333+
{
3334+
message: "discriminator object is missing required `propertyName` field",
3335+
path: "$.components.schemas['Pet'].discriminator",
3336+
},
3337+
},
3338+
},
3339+
{
3340+
name: "NoDiscriminator",
3341+
yaml: `openapi: 3.0.3
3342+
components:
3343+
schemas:
3344+
Pet:
3345+
type: object
3346+
properties:
3347+
name:
3348+
type: string
3349+
age:
3350+
type: integer`,
3351+
expectedErrors: []struct{ message, path string }{},
3352+
},
3353+
{
3354+
name: "WithoutMapping",
3355+
yaml: `openapi: 3.0.3
3356+
components:
3357+
schemas:
3358+
Pet:
3359+
type: object
3360+
properties:
3361+
petType:
3362+
type: string
3363+
name:
3364+
type: string
3365+
discriminator:
3366+
propertyName: petType`,
3367+
expectedErrors: []struct{ message, path string }{},
3368+
},
3369+
}
3370+
3371+
for _, tt := range tests {
3372+
t.Run(tt.name, func(t *testing.T) {
3373+
document, err := libopenapi.NewDocument([]byte(tt.yaml))
3374+
if err != nil {
3375+
t.Fatalf("cannot create document: %v", err)
3376+
}
3377+
3378+
m, _ := document.BuildV3Model()
3379+
drDocument := drModel.NewDrDocument(m)
3380+
3381+
rule := buildOpenApiTestRuleAction("$", "schema-type-check", "", nil)
3382+
ctx := buildOpenApiTestContext(model.CastToRuleAction(rule.Then), nil)
3383+
3384+
ctx.Document = document
3385+
ctx.DrDocument = drDocument
3386+
ctx.Rule = &rule
3387+
3388+
def := SchemaTypeCheck{}
3389+
res := def.RunRule(nil, ctx)
3390+
3391+
assert.Len(t, res, len(tt.expectedErrors))
3392+
for i, expectedErr := range tt.expectedErrors {
3393+
if i < len(res) {
3394+
assert.Equal(t, expectedErr.message, res[i].Message)
3395+
assert.Equal(t, expectedErr.path, res[i].Path)
3396+
}
3397+
}
3398+
})
3399+
}
3400+
}
3401+

0 commit comments

Comments
 (0)