Skip to content

Commit decfda9

Browse files
authored
Fix for #389. Also fixes the special situation of nested discriminators not working correctly when the parent schema has a discriminator, but is not referenced via anyOf/ (#399)
1 parent 6c573d4 commit decfda9

File tree

5 files changed

+289
-28
lines changed

5 files changed

+289
-28
lines changed

src/main/java/com/networknt/schema/AllOfValidator.java

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,22 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
6363
if (null != $ref) {
6464
final ValidationContext.DiscriminatorContext currentDiscriminatorContext = validationContext
6565
.getCurrentDiscriminatorContext();
66-
final ObjectNode discriminator = currentDiscriminatorContext
67-
.getDiscriminatorForPath(allOfEntry.get("$ref").asText());
68-
if (null != discriminator) {
69-
registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, parentSchema, at);
70-
// now we have to check whether we have hit the right target
71-
final String discriminatorPropertyName = discriminator.get("propertyName").asText();
72-
final String discriminatorPropertyValue = node.get(discriminatorPropertyName).textValue();
73-
74-
final JsonSchema jsonSchema = parentSchema;
75-
checkDiscriminatorMatch(
76-
currentDiscriminatorContext,
77-
discriminator,
78-
discriminatorPropertyValue,
79-
jsonSchema);
66+
if (null != currentDiscriminatorContext) {
67+
final ObjectNode discriminator = currentDiscriminatorContext
68+
.getDiscriminatorForPath(allOfEntry.get("$ref").asText());
69+
if (null != discriminator) {
70+
registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, parentSchema, at);
71+
// now we have to check whether we have hit the right target
72+
final String discriminatorPropertyName = discriminator.get("propertyName").asText();
73+
final String discriminatorPropertyValue = node.get(discriminatorPropertyName).textValue();
74+
75+
final JsonSchema jsonSchema = parentSchema;
76+
checkDiscriminatorMatch(
77+
currentDiscriminatorContext,
78+
discriminator,
79+
discriminatorPropertyValue,
80+
jsonSchema);
81+
}
8082
}
8183
}
8284
}
@@ -86,8 +88,6 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
8688
return Collections.unmodifiableSet(errors);
8789
}
8890

89-
90-
9191
@Override
9292
public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
9393
Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();

src/main/java/com/networknt/schema/AnyOfValidator.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
8383
}
8484
}
8585
allErrors.addAll(errors);
86-
// allErrors.add(renderMissingMatchValidationMessage(at));
86+
}
87+
88+
if (config.isOpenAPI3StyleDiscriminators() && discriminatorContext.isActive()) {
89+
final Set<ValidationMessage> errors = new HashSet<ValidationMessage>();
90+
errors.add(buildValidationMessage(at, "based on the provided discriminator. No alternative could be chosen based on the discriminator property"));
91+
return Collections.unmodifiableSet(errors);
8792
}
8893
} finally {
8994
if (config.isOpenAPI3StyleDiscriminators()) {
@@ -92,11 +97,4 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
9297
}
9398
return Collections.unmodifiableSet(allErrors);
9499
}
95-
96-
private ValidationMessage renderMissingMatchValidationMessage(final String at) {
97-
if (config.isOpenAPI3StyleDiscriminators()) {
98-
return super.buildValidationMessage(at, "and no match could be found (respecting discriminators). " + REMARK);
99-
}
100-
return super.buildValidationMessage(at, "and no match could be found. " + REMARK);
101-
}
102100
}

src/main/java/com/networknt/schema/JsonSchema.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private JsonSchema(ValidationContext validationContext, String schemaPath, URI c
8282

8383
if (config.isOpenAPI3StyleDiscriminators()) {
8484
ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator");
85-
if (null != discriminator) {
85+
if (null != discriminator && null != validationContext.getCurrentDiscriminatorContext()) {
8686
validationContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaPath, discriminator);
8787
}
8888
}
@@ -245,11 +245,22 @@ public Set<ValidationMessage> validate(JsonNode jsonNode, JsonNode rootNode, Str
245245
if (null != discriminator) {
246246
final DiscriminatorContext discriminatorContext = validationContext.getCurrentDiscriminatorContext();
247247
if (null != discriminatorContext) {
248+
final ObjectNode discriminatorToUse;
248249
final ObjectNode discriminatorFromContext = discriminatorContext.getDiscriminatorForPath(schemaPath);
249-
final String discriminatorPropertyName = discriminatorFromContext.get("propertyName").asText();
250+
if (null == discriminatorFromContext) {
251+
// register the current discriminator. This can only happen when the current context discriminator
252+
// was not registered via allOf. In that case we have a $ref to the schema with discriminator that gets
253+
// used for validation before allOf validation has kicked in
254+
discriminatorContext.registerDiscriminator(schemaPath, discriminator);
255+
discriminatorToUse = discriminator;
256+
} else{
257+
discriminatorToUse = discriminatorFromContext;
258+
}
259+
260+
final String discriminatorPropertyName = discriminatorToUse.get("propertyName").asText();
250261
final String discriminatorPropertyValue = jsonNode.get(discriminatorPropertyName).asText();
251262
checkDiscriminatorMatch(discriminatorContext,
252-
discriminatorFromContext,
263+
discriminatorToUse,
253264
discriminatorPropertyValue,
254265
this);
255266
}

src/main/java/com/networknt/schema/ValidationContext.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ public JsonSchemaRef getReferenceParsingInProgress(String refValue) {
9090
}
9191

9292
public DiscriminatorContext getCurrentDiscriminatorContext() {
93-
return discriminatorContexts.peek();
93+
if (!discriminatorContexts.empty()) {
94+
return discriminatorContexts.peek();
95+
}
96+
return null; // this is the case when we get on a schema that has a discriminator, but it's not used in anyOf
9497
}
9598

9699
public void enterDiscriminatorContext(final DiscriminatorContext ctx, String at) {
@@ -125,5 +128,14 @@ public void markMatch() {
125128
public boolean isDiscriminatorMatchFound() {
126129
return discriminatorMatchFound;
127130
}
131+
132+
/**
133+
* Returns true if we have a discriminator active. In this case no valid match in anyOf should lead to validation failure
134+
*
135+
* @return true in case there are discriminator candidates
136+
*/
137+
public boolean isActive() {
138+
return !discriminators.isEmpty();
139+
}
128140
}
129141
}

src/test/resources/openapi3/discriminator.json

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,5 +279,245 @@
279279
"valid": false
280280
}
281281
]
282+
},
283+
{
284+
"description": "Issue #398 - $ref pointing to base type with discriminator and w/o anyOf",
285+
"schema": {
286+
"$ref": "#/components/schemas/Room",
287+
"components": {
288+
"schemas": {
289+
"Room": {
290+
"type": "object",
291+
"properties": {
292+
"@type": {
293+
"type": "string"
294+
},
295+
"floor": {
296+
"type": "integer"
297+
},
298+
"nextRoom": {
299+
"anyOf": [
300+
{"$ref":"#/components/schemas/Room" },
301+
{"$ref":"#/components/schemas/BedRoom" }
302+
]
303+
}
304+
},
305+
"required": [
306+
"@type"
307+
],
308+
"discriminator": {
309+
"propertyName": "@type"
310+
}
311+
},
312+
"BedRoom": {
313+
"type": "object",
314+
"allOf": [
315+
{
316+
"$ref": "#/components/schemas/Room"
317+
},
318+
{
319+
"type": "object",
320+
"properties": {
321+
"numberOfBeds": {
322+
"type": "integer"
323+
}
324+
},
325+
"required": [
326+
"numberOfBeds"
327+
]
328+
}
329+
]
330+
}
331+
}
332+
}
333+
},
334+
"tests": [
335+
{
336+
"description": "$ref without anyOf and therefore no discriminator context",
337+
"data": {
338+
"@type": "Room"
339+
},
340+
"valid": true
341+
},
342+
{
343+
"description": "schema with discriminator and recursion with invalid BedRoom",
344+
"data": {
345+
"@type": "can be ignored - discriminator not in use on root schema",
346+
"numberOfBeds": 42,
347+
"nextRoom": {
348+
"@type": "BedRoom",
349+
"floor": 1
350+
}
351+
},
352+
"valid": false
353+
}
354+
]
355+
},
356+
{
357+
"description": "Issue #398 - $ref pointing to extended type with discriminator and w/o anyOf",
358+
"schema": {
359+
"$ref": "#/components/schemas/BedRoom",
360+
"components": {
361+
"schemas": {
362+
"Room": {
363+
"type": "object",
364+
"properties": {
365+
"@type": {
366+
"type": "string"
367+
},
368+
"floor": {
369+
"type": "integer"
370+
},
371+
"nextRoom": {
372+
"anyOf": [
373+
{"$ref": "#/components/schemas/Room"},
374+
{"$ref": "#/components/schemas/BedRoom"}
375+
]
376+
}
377+
},
378+
"required": [
379+
"@type"
380+
],
381+
"discriminator": {
382+
"propertyName": "@type"
383+
}
384+
},
385+
"BedRoom": {
386+
"type": "object",
387+
"allOf": [
388+
{
389+
"$ref": "#/components/schemas/Room"
390+
},
391+
{
392+
"type": "object",
393+
"properties": {
394+
"numberOfBeds": {
395+
"type": "integer"
396+
},
397+
"nextRoom": {
398+
"anyOf": [
399+
{"$ref": "#/components/schemas/Room"},
400+
{"$ref": "#/components/schemas/BedRoom"}
401+
]
402+
}
403+
},
404+
"required": [
405+
"numberOfBeds"
406+
]
407+
}
408+
]
409+
}
410+
}
411+
}
412+
},
413+
"tests": [
414+
{
415+
"description": "$ref without anyOf and therefore no discriminator context",
416+
"data": {
417+
"@type": "can be ignored - discriminator not in use on root schema",
418+
"numberOfBeds": 42,
419+
"nextRoom": {
420+
"@type": "Room",
421+
"floor": 3
422+
}
423+
},
424+
"valid": true
425+
},
426+
{
427+
"description": "schema with discriminator and recursion with valid BedRoom",
428+
"data": {
429+
"@type": "can be ignored - discriminator not in use on root schema",
430+
"numberOfBeds": 42,
431+
"nextRoom": {
432+
"@type": "BedRoom",
433+
"floor": 1,
434+
"numberOfBeds": 12345
435+
}
436+
},
437+
"valid": true
438+
},
439+
{
440+
"description": "schema with discriminator and recursion with invalid BedRoom",
441+
"data": {
442+
"@type": "can be ignored - discriminator not in use on root schema",
443+
"numberOfBeds": 42,
444+
"nextRoom": {
445+
"@type": "BedRoom",
446+
"floor": 1
447+
}
448+
},
449+
"valid": false
450+
}
451+
]
452+
},
453+
{
454+
"description": "Issue #398 - invalid discriminator specification has to lead to failed validation",
455+
"schema": {
456+
"anyOf": [
457+
{"$ref": "#/components/schemas/Room"},
458+
{"$ref": "#/components/schemas/BedRoom"}
459+
],
460+
"components": {
461+
"schemas": {
462+
"Room": {
463+
"type": "object",
464+
"properties": {
465+
"@type": {
466+
"type": "string"
467+
},
468+
"floor": {
469+
"type": "integer"
470+
},
471+
"nextRoom": {
472+
"anyOf": [
473+
{"$ref": "#/components/schemas/Room"},
474+
{"$ref": "#/components/schemas/BedRoom"}
475+
]
476+
}
477+
},
478+
"required": [
479+
"@type"
480+
],
481+
"discriminator": {
482+
"propertyName": "@type"
483+
}
484+
},
485+
"BedRoom": {
486+
"type": "object",
487+
"allOf": [
488+
{
489+
"$ref": "#/components/schemas/Room"
490+
},
491+
{
492+
"type": "object",
493+
"properties": {
494+
"numberOfBeds": {
495+
"type": "integer"
496+
},
497+
"nextRoom": {
498+
"anyOf": [
499+
{"$ref": "#/components/schemas/Room"},
500+
{"$ref": "#/components/schemas/BedRoom"}
501+
]
502+
}
503+
},
504+
"required": [
505+
"numberOfBeds"
506+
]
507+
}
508+
]
509+
}
510+
}
511+
}
512+
},
513+
"tests": [
514+
{
515+
"description": "$ref without anyOf and therefore no discriminator context",
516+
"data": {
517+
"@type": "illegal discriminator property value"
518+
},
519+
"valid": false
520+
}
521+
]
282522
}
283523
]

0 commit comments

Comments
 (0)