Skip to content

Commit f59d413

Browse files
committed
First test case with oneOf passing
1 parent 26b28b1 commit f59d413

File tree

9 files changed

+420
-71
lines changed

9 files changed

+420
-71
lines changed

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,34 @@ public static JsonMetaSchema getInstance() {
8888
}
8989
}
9090

91+
private static class OPENAI_V3_0_3 {
92+
private static String URI = "https://json-schema.org/draft-04/schema";
93+
private static final String ID = "id";
94+
95+
public static final List<Format> BUILTIN_FORMATS = new ArrayList<Format>(JsonMetaSchema.COMMON_BUILTIN_FORMATS);
96+
97+
static {
98+
99+
}
100+
101+
public static JsonMetaSchema getInstance() {
102+
return new Builder(URI)
103+
.idKeyword(ID)
104+
.addFormats(BUILTIN_FORMATS)
105+
.addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.OPENAPI_3_0_3))
106+
// keywords that may validly exist, but have no validation aspect to them
107+
.addKeywords(Arrays.asList(
108+
new NonValidationKeyword("$schema"),
109+
new NonValidationKeyword("id"),
110+
new NonValidationKeyword("title"),
111+
new NonValidationKeyword("description"),
112+
new NonValidationKeyword("default"),
113+
new NonValidationKeyword("definitions")
114+
))
115+
.build();
116+
}
117+
}
118+
91119
private static class V6 {
92120
private static String URI = "https://json-schema.org/draft-06/schema";
93121
// Draft 6 uses "$id"
@@ -289,6 +317,10 @@ public static JsonMetaSchema getV201909() {
289317
return V201909.getInstance();
290318
}
291319

320+
public static JsonMetaSchema getOpenAPI3_0_3 () {
321+
return OPENAI_V3_0_3.getInstance();
322+
}
323+
292324
/**
293325
* Builder without keywords or formats.
294326
* <p>

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.networknt.schema;
1818

1919
import com.fasterxml.jackson.databind.JsonNode;
20+
import com.fasterxml.jackson.databind.node.ObjectNode;
2021
import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner;
2122
import com.networknt.schema.walk.JsonSchemaWalker;
2223
import com.networknt.schema.walk.WalkListenerRunner;
@@ -199,10 +200,23 @@ private Map<String, JsonValidator> read(JsonNode schemaNode) {
199200
}
200201
} else {
201202
Iterator<String> pnames = schemaNode.fieldNames();
203+
JsonNode discriminator = null;
204+
while(pnames.hasNext()) {
205+
if (pnames.next().equals("discriminator")) {
206+
discriminator = schemaNode.get("discriminator");
207+
break;
208+
}
209+
}
210+
211+
pnames = schemaNode.fieldNames();
202212
while (pnames.hasNext()) {
203213
String pname = pnames.next();
204214
JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname);
205215

216+
if (pname.equals("oneOf") && null != discriminator) {
217+
validationContext.registerDiscriminator(nodeToUse, discriminator);
218+
}
219+
206220
JsonValidator validator = validationContext.newValidator(getSchemaPath(), pname, nodeToUse, this);
207221
if (validator != null) {
208222
validators.put(getSchemaPath() + "/" + pname, validator);

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static class Builder {
4747
private URNFactory urnFactory;
4848
private final Map<String, JsonMetaSchema> jsonMetaSchemas = new HashMap<String, JsonMetaSchema>();
4949
private final Map<String, String> uriMap = new HashMap<String, String>();
50-
50+
5151

5252
public Builder() {
5353
// Adds support for creating {@link URL}s.
@@ -138,8 +138,8 @@ public Builder addUrnFactory(URNFactory urnFactory) {
138138
this.urnFactory = urnFactory;
139139
return this;
140140
}
141-
142-
141+
142+
143143

144144
public JsonSchemaFactory build() {
145145
// create builtin keywords with (custom) formats.
@@ -237,6 +237,9 @@ public static JsonSchemaFactory getInstance(SpecVersion.VersionFlag versionFlag)
237237
case V4:
238238
metaSchema = JsonMetaSchema.getV4();
239239
break;
240+
case OPENAPI_3_0_3:
241+
metaSchema = JsonMetaSchema.getOpenAPI3_0_3();
242+
break;
240243
}
241244
return builder()
242245
.defaultMetaSchemaURI(metaSchema.getUri())

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

Lines changed: 96 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,50 @@
1616

1717
package com.networknt.schema;
1818

19+
import java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.HashSet;
23+
import java.util.Iterator;
24+
import java.util.LinkedHashSet;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.Set;
28+
import java.util.regex.Matcher;
29+
import java.util.regex.Pattern;
30+
1931
import com.fasterxml.jackson.databind.JsonNode;
2032
import org.slf4j.Logger;
2133
import org.slf4j.LoggerFactory;
2234

23-
import java.util.*;
24-
2535
public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
2636
private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
37+
public static final Pattern TYPE_PATTERN = Pattern.compile("^#(/[a-zA-Z0-9-_/]*)/([a-zA-Z0-9_]+)");
2738

28-
private List<ShortcutValidator> schemas = new ArrayList<ShortcutValidator>();
39+
private final List<ShortcutValidator> schemas = new ArrayList<ShortcutValidator>();
2940

3041
private static class ShortcutValidator {
3142
private final JsonSchema schema;
3243
private final Map<String, String> constants;
33-
34-
ShortcutValidator(JsonNode schemaNode, JsonSchema parentSchema,
35-
ValidationContext validationContext, JsonSchema schema) {
44+
private final JsonNode discriminator;
45+
46+
ShortcutValidator(JsonNode schemaNode,
47+
JsonSchema parentSchema,
48+
ValidationContext validationContext,
49+
JsonSchema schema,
50+
JsonNode discriminator) {
51+
this.discriminator = discriminator;
3652
JsonNode refNode = schemaNode.get(ValidatorTypeCode.REF.getValue());
37-
JsonSchema resolvedRefSchema = refNode != null && refNode.isTextual() ? RefValidator.getRefSchema(parentSchema, validationContext, refNode.textValue()).getSchema() : null;
53+
JsonSchema resolvedRefSchema = refNode != null && refNode.isTextual() ? RefValidator
54+
.getRefSchema(parentSchema, validationContext, refNode.textValue()).getSchema() : null;
3855
this.constants = extractConstants(schemaNode, resolvedRefSchema);
3956
this.schema = schema;
4057
}
4158

4259
private Map<String, String> extractConstants(JsonNode schemaNode, JsonSchema resolvedRefSchema) {
43-
Map<String, String> refMap = resolvedRefSchema != null ? extractConstants(resolvedRefSchema.getSchemaNode()) : Collections.<String, String>emptyMap();
60+
Map<String, String> refMap = resolvedRefSchema != null
61+
? extractConstants(resolvedRefSchema.getSchemaNode())
62+
: Collections.<String, String>emptyMap();
4463
Map<String, String> schemaMap = extractConstants(schemaNode);
4564
if (refMap.isEmpty()) {
4665
return schemaMap;
@@ -115,11 +134,16 @@ private JsonSchema getSchema() {
115134

116135
public OneOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
117136
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF, validationContext);
137+
JsonNode discriminator = validationContext.getDiscriminatorForCurrentSchemaNode(schemaNode);
118138
int size = schemaNode.size();
119139
for (int i = 0; i < size; i++) {
120140
JsonNode childNode = schemaNode.get(i);
121-
JsonSchema childSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), childNode, parentSchema);
122-
schemas.add(new ShortcutValidator(childNode, parentSchema, validationContext, childSchema));
141+
JsonSchema childSchema = new JsonSchema(validationContext,
142+
getValidatorType().getValue(),
143+
parentSchema.getCurrentUri(),
144+
childNode,
145+
parentSchema);
146+
schemas.add(new ShortcutValidator(childNode, parentSchema, validationContext, childSchema, discriminator));
123147
}
124148

125149
parseErrorCode(getValidatorType().getErrorCodeKey());
@@ -133,14 +157,15 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
133157
state.setComplexValidator(true);
134158

135159
int numberOfValidSchema = 0;
160+
int skippedSchemas = 0;
136161
Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
137162
Set<ValidationMessage> childErrors = new LinkedHashSet<ValidationMessage>();
138163
// validate that only a single element has been received in the oneOf node
139164
// validation should not continue, as it contradicts the oneOf requirement of only one
140-
// if(node.isObject() && node.size()>1) {
141-
// errors = Collections.singleton(buildValidationMessage(at, ""));
142-
// return Collections.unmodifiableSet(errors);
143-
// }
165+
// if(node.isObject() && node.size()>1) {
166+
// errors = Collections.singleton(buildValidationMessage(at, ""));
167+
// return Collections.unmodifiableSet(errors);
168+
// }
144169

145170
for (ShortcutValidator validator : schemas) {
146171
Set<ValidationMessage> schemaErrors = null;
@@ -153,9 +178,38 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
153178
// we can bail out of the validation
154179
continue;
155180
}*/
181+
JsonNode discriminator = validator.discriminator;
182+
if (null != discriminator) {
183+
final String propertyName = discriminator.get("propertyName").textValue();
184+
final String identifiedSchemaKey = node.get(propertyName).textValue();
185+
final JsonNode discriminatorMapping = discriminator.get("mapping");
186+
final String discriminatorDeterminedSchemaURL;
187+
final String currentValidatorSchemaLocation = validator.getSchema().schemaNode.get("$ref").textValue();
188+
189+
if (null == discriminatorMapping || discriminatorMapping.get(identifiedSchemaKey) == null) {
190+
if (currentValidatorSchemaLocation.startsWith("#")) {
191+
final Matcher matcher = TYPE_PATTERN.matcher(currentValidatorSchemaLocation);
192+
if (matcher.matches() && matcher.group(2).equals(identifiedSchemaKey)) {
193+
discriminatorDeterminedSchemaURL = currentValidatorSchemaLocation;
194+
} else {
195+
discriminatorDeterminedSchemaURL = null;
196+
}
197+
} else {
198+
throw new UnsupportedOperationException("Remote schema support for discriminators not yet implemented"); // TODO
199+
}
200+
} else {
201+
discriminatorDeterminedSchemaURL = discriminatorMapping.get(identifiedSchemaKey).textValue();
202+
}
203+
204+
if (!currentValidatorSchemaLocation.equals(discriminatorDeterminedSchemaURL)) {
205+
skippedSchemas++;
206+
continue; // next validator
207+
}
208+
}
156209

157210
// get the current validator
158211
JsonSchema schema = validator.schema;
212+
159213
if (!state.isWalkEnabled()) {
160214
schemaErrors = schema.validate(node, rootNode, at);
161215
} else {
@@ -165,19 +219,28 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
165219
// check if any validation errors have occurred
166220
if (schemaErrors.isEmpty()) {
167221
// check whether there are no errors HOWEVER we have validated the exact validator
168-
if (!state.hasMatchedNode())
222+
if (!state.hasMatchedNode()) {
169223
continue;
224+
}
170225

171226
numberOfValidSchema++;
172227
}
173228
childErrors.addAll(schemaErrors);
174229
}
175230

231+
if (skippedSchemas == schemas.size()) {
232+
// this is when no discriminator value matched, so we have not a single candidate to check
233+
final ValidationMessage message = getNoMatchingDiscriminatorErrorMsg(at);
234+
if (failFast) {
235+
throw new JsonSchemaException(message);
236+
}
237+
errors.add(message);
238+
}
176239

177240
// ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1.
178-
if(numberOfValidSchema > 1){
241+
else if (numberOfValidSchema > 1) {
179242
final ValidationMessage message = getMultiSchemasValidErrorMsg(at);
180-
if( failFast ) {
243+
if (failFast) {
181244
throw new JsonSchemaException(message);
182245
}
183246
errors.add(message);
@@ -187,14 +250,15 @@ else if (numberOfValidSchema < 1) {
187250
if (!childErrors.isEmpty()) {
188251
errors.addAll(childErrors);
189252
}
190-
if( failFast ){
253+
if (failFast) {
191254
throw new JsonSchemaException(errors.toString());
192255
}
193256
}
194257

195258
// Make sure to signal parent handlers we matched
196-
if (errors.isEmpty())
259+
if (errors.isEmpty()) {
197260
state.setMatchedNode(true);
261+
}
198262

199263
// reset the ValidatorState object in the ThreadLocal
200264
resetValidatorState();
@@ -210,7 +274,7 @@ private void resetValidatorState() {
210274

211275
public List<JsonSchema> getChildSchemas() {
212276
List<JsonSchema> childJsonSchemas = new ArrayList<JsonSchema>();
213-
for (ShortcutValidator shortcutValidator: schemas ) {
277+
for (ShortcutValidator shortcutValidator : schemas) {
214278
childJsonSchemas.add(shortcutValidator.getSchema());
215279
}
216280
return childJsonSchemas;
@@ -223,24 +287,29 @@ public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at,
223287
validationMessages.addAll(validate(node, rootNode, at));
224288
} else {
225289
for (ShortcutValidator validator : schemas) {
226-
validator.schema.walk(node, rootNode, at , shouldValidateSchema);
290+
validator.schema.walk(node, rootNode, at, shouldValidateSchema);
227291
}
228292
}
229293
return validationMessages;
230294
}
231295

232-
private ValidationMessage getMultiSchemasValidErrorMsg(String at){
233-
String msg="";
234-
for(ShortcutValidator schema: schemas){
296+
private ValidationMessage getMultiSchemasValidErrorMsg(String at) {
297+
String msg = "";
298+
for (ShortcutValidator schema : schemas) {
235299
String schemaValue = schema.getSchema().getSchemaNode().toString();
236300
msg = msg.concat(schemaValue);
237301
}
238302

239-
ValidationMessage message = ValidationMessage.of(getValidatorType().getValue(),ValidatorTypeCode.ONE_OF ,
240-
at, String.format("but more than one schemas {%s} are valid ",msg));
303+
ValidationMessage message = ValidationMessage.of(getValidatorType().getValue(), ValidatorTypeCode.ONE_OF,
304+
at, String.format("but more than one schemas {%s} are valid ", msg));
241305

242306
return message;
243307
}
244308

245-
309+
private ValidationMessage getNoMatchingDiscriminatorErrorMsg(String at) {
310+
311+
return ValidationMessage.of(getValidatorType().getValue(), ValidatorTypeCode.ONE_OF,
312+
at, "but no candidate schema can be identified via the discriminator configuration");
313+
}
314+
246315
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public enum VersionFlag {
2525
V4(1 << 0),
2626
V6(1 << 1),
2727
V7(1 << 2),
28-
V201909(1 << 3);
28+
V201909(1 << 3),
29+
OPENAPI_3_0_3(1 << 4);
2930

3031

3132
private final long versionFlagValue;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.networknt.schema.uri.URIFactory;
2121
import com.networknt.schema.urn.URNFactory;
2222

23+
import java.util.ArrayList;
2324
import java.util.HashMap;
2425
import java.util.Map;
2526

@@ -30,6 +31,7 @@ public class ValidationContext {
3031
private final JsonSchemaFactory jsonSchemaFactory;
3132
private SchemaValidatorsConfig config;
3233
private final Map<String, JsonSchemaRef> refParsingInProgress = new HashMap<String, JsonSchemaRef>();
34+
private Map<JsonNode, JsonNode> discriminators = null;
3335

3436
public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema,
3537
JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) {
@@ -86,6 +88,17 @@ public JsonSchemaRef getReferenceParsingInProgress(String refValue) {
8688
return refParsingInProgress.get(refValue);
8789
}
8890

91+
public void registerDiscriminator(JsonNode schemaNode, JsonNode discriminator) {
92+
if (discriminators == null) {
93+
discriminators = new HashMap<JsonNode, JsonNode>();
94+
}
95+
discriminators.put(schemaNode, discriminator);
96+
}
97+
98+
public JsonNode getDiscriminatorForCurrentSchemaNode(JsonNode schemaNode) {
99+
return discriminators.get(schemaNode);
100+
}
101+
89102
protected JsonMetaSchema getMetaSchema() {
90103
return metaSchema;
91104
}

0 commit comments

Comments
 (0)