• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2016 Network New Technologies Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.networknt.schema;
18 
19 import com.fasterxml.jackson.core.JsonProcessingException;
20 import com.fasterxml.jackson.databind.JsonNode;
21 import com.fasterxml.jackson.databind.node.ObjectNode;
22 import com.networknt.schema.SpecVersion.VersionFlag;
23 import com.networknt.schema.serialization.JsonMapperFactory;
24 import com.networknt.schema.serialization.YamlMapperFactory;
25 import com.networknt.schema.utils.JsonNodes;
26 import com.networknt.schema.utils.SetView;
27 
28 import java.io.UnsupportedEncodingException;
29 import java.net.URLDecoder;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.Comparator;
33 import java.util.Iterator;
34 import java.util.LinkedHashSet;
35 import java.util.List;
36 import java.util.Map.Entry;
37 import java.util.Objects;
38 import java.util.Set;
39 import java.util.function.Consumer;
40 
41 /**
42  * Used for creating a schema with validators for validating inputs. This is
43  * created using {@link JsonSchemaFactory#getInstance(VersionFlag, Consumer)}
44  * and should be cached for performance.
45  * <p>
46  * This is the core of json constraint implementation. It parses json constraint
47  * file and generates JsonValidators. The class is thread safe, once it is
48  * constructed, it can be used to validate multiple json data concurrently.
49  */
50 public class JsonSchema extends BaseJsonValidator {
51     private static final long V201909_VALUE = VersionFlag.V201909.getVersionFlagValue();
52 
53     /**
54      * The validators sorted and indexed by evaluation path.
55      */
56     private List<JsonValidator> validators;
57     private boolean validatorsLoaded = false;
58     private boolean recursiveAnchor = false;
59 
60     private JsonValidator requiredValidator = null;
61     private TypeValidator typeValidator;
62 
63     private final String id;
64 
from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval)65     static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) {
66         return new JsonSchema(validationContext, schemaLocation, evaluationPath, schemaNode, parent, suppressSubSchemaRetrieval);
67     }
68 
hasNoFragment(SchemaLocation schemaLocation)69     private boolean hasNoFragment(SchemaLocation schemaLocation) {
70         return this.schemaLocation.getFragment() == null || this.schemaLocation.getFragment().getNameCount() == 0;
71     }
72 
resolve(SchemaLocation schemaLocation, JsonNode schemaNode, boolean rootSchema, ValidationContext validationContext)73     private static SchemaLocation resolve(SchemaLocation schemaLocation, JsonNode schemaNode, boolean rootSchema,
74             ValidationContext validationContext) {
75         String id = validationContext.resolveSchemaId(schemaNode);
76         if (id != null) {
77             String resolve = id;
78             int fragment = id.indexOf('#');
79             // Check if there is a non-empty fragment
80             if (fragment != -1 && !(fragment + 1 >= id.length())) {
81                 // strip the fragment when resolving
82                 resolve = id.substring(0, fragment);
83             }
84             SchemaLocation result = !"".equals(resolve) ? schemaLocation.resolve(resolve) : schemaLocation;
85             JsonSchemaIdValidator validator = validationContext.getConfig().getSchemaIdValidator();
86             if (validator != null) {
87                 if (!validator.validate(id, rootSchema, schemaLocation, result, validationContext)) {
88                     SchemaLocation idSchemaLocation = schemaLocation.append(validationContext.getMetaSchema().getIdKeyword());
89                     ValidationMessage validationMessage = ValidationMessage.builder()
90                             .code(ValidatorTypeCode.ID.getValue()).type(ValidatorTypeCode.ID.getValue())
91                             .instanceLocation(idSchemaLocation.getFragment())
92                             .arguments(id, validationContext.getMetaSchema().getIdKeyword(), idSchemaLocation)
93                             .schemaLocation(idSchemaLocation)
94                             .schemaNode(schemaNode)
95                             .messageFormatter(args -> validationContext.getConfig().getMessageSource().getMessage(
96                                     ValidatorTypeCode.ID.getValue(), validationContext.getConfig().getLocale(), args))
97                             .build();
98                     throw new InvalidSchemaException(validationMessage);
99                 }
100             }
101             return result;
102         } else {
103             return schemaLocation;
104         }
105     }
106 
JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval)107     private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath,
108             JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) {
109         super(resolve(schemaLocation, schemaNode, parent == null, validationContext), evaluationPath, schemaNode, parent,
110                 null, null, validationContext, suppressSubSchemaRetrieval);
111         String id = this.validationContext.resolveSchemaId(this.schemaNode);
112         if (id != null) {
113             // In earlier drafts $id may contain an anchor fragment see draft4/idRef.json
114             // Note that json pointer fragments in $id are not allowed
115             SchemaLocation result = id.contains("#") ? schemaLocation.resolve(id) : this.schemaLocation;
116             if (hasNoFragment(result)) {
117                 this.id = id;
118             } else {
119                 // This is an anchor fragment and is not a document
120                 // This will be added to schema resources later
121                 this.id = null;
122             }
123             this.validationContext.getSchemaResources().putIfAbsent(result != null ? result.toString() : id, this);
124         } else {
125             if (hasNoFragment(schemaLocation)) {
126                 // No $id but there is no fragment and is thus a schema resource
127                 this.id = schemaLocation.getAbsoluteIri() != null ? schemaLocation.getAbsoluteIri().toString() : "";
128                 this.validationContext.getSchemaResources()
129                         .putIfAbsent(schemaLocation != null ? schemaLocation.toString() : this.id, this);
130             } else {
131                 this.id = null;
132             }
133         }
134         String anchor = this.validationContext.getMetaSchema().readAnchor(this.schemaNode);
135         if (anchor != null) {
136             String absoluteIri = this.schemaLocation.getAbsoluteIri() != null
137                     ? this.schemaLocation.getAbsoluteIri().toString()
138                     : "";
139             this.validationContext.getSchemaResources()
140                     .putIfAbsent(absoluteIri + "#" + anchor, this);
141         }
142         String dynamicAnchor = this.validationContext.getMetaSchema().readDynamicAnchor(schemaNode);
143         if (dynamicAnchor != null) {
144             String absoluteIri = this.schemaLocation.getAbsoluteIri() != null
145                     ? this.schemaLocation.getAbsoluteIri().toString()
146                     : "";
147             this.validationContext.getDynamicAnchors()
148                     .putIfAbsent(absoluteIri + "#" + dynamicAnchor, this);
149         }
150         getValidators();
151     }
152 
153     /**
154      * Copy constructor.
155      *
156      * @param copy to copy from
157      */
JsonSchema(JsonSchema copy)158     protected JsonSchema(JsonSchema copy) {
159         super(copy);
160         this.validators = copy.validators;
161         this.validatorsLoaded = copy.validatorsLoaded;
162         this.recursiveAnchor = copy.recursiveAnchor;
163         this.requiredValidator = copy.requiredValidator;
164         this.typeValidator = copy.typeValidator;
165         this.id = copy.id;
166     }
167 
168     /**
169      * Creates a schema using the current one as a template with the parent as the
170      * ref.
171      * <p>
172      * This is typically used if this schema is a schema resource that can be
173      * pointed to by various references.
174      *
175      * @param refEvaluationParentSchema the parent ref
176      * @param refEvaluationPath the ref evaluation path
177      * @return the schema
178      */
fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath refEvaluationPath)179     public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath refEvaluationPath) {
180         JsonSchema copy = new JsonSchema(this);
181         copy.validationContext = new ValidationContext(copy.getValidationContext().getMetaSchema(),
182                 copy.getValidationContext().getJsonSchemaFactory(),
183                 refEvaluationParentSchema.validationContext.getConfig(),
184                 refEvaluationParentSchema.getValidationContext().getSchemaReferences(),
185                 refEvaluationParentSchema.getValidationContext().getSchemaResources(),
186                 refEvaluationParentSchema.getValidationContext().getDynamicAnchors());
187         copy.evaluationPath = refEvaluationPath;
188         copy.evaluationParentSchema = refEvaluationParentSchema;
189         // Validator state is reset due to the changes in evaluation path
190         copy.validatorsLoaded = false;
191         copy.requiredValidator = null;
192         copy.typeValidator = null;
193         copy.validators = null;
194         return copy;
195     }
196 
withConfig(SchemaValidatorsConfig config)197     public JsonSchema withConfig(SchemaValidatorsConfig config) {
198         if (!this.getValidationContext().getConfig().equals(config)) {
199             JsonSchema copy = new JsonSchema(this);
200             copy.validationContext = new ValidationContext(copy.getValidationContext().getMetaSchema(),
201                     copy.getValidationContext().getJsonSchemaFactory(), config,
202                     copy.getValidationContext().getSchemaReferences(),
203                     copy.getValidationContext().getSchemaResources(),
204                     copy.getValidationContext().getDynamicAnchors());
205             copy.validatorsLoaded = false;
206             copy.requiredValidator = null;
207             copy.typeValidator = null;
208             copy.validators = null;
209             return copy;
210         }
211         return this;
212     }
213 
getValidationContext()214     public ValidationContext getValidationContext() {
215         return this.validationContext;
216     }
217 
218     /**
219      * Find the schema node for $ref attribute.
220      *
221      * @param ref String
222      * @return JsonNode
223      */
getRefSchemaNode(String ref)224     public JsonNode getRefSchemaNode(String ref) {
225         JsonSchema schema = findSchemaResourceRoot();
226         JsonNode node = schema.getSchemaNode();
227 
228         String jsonPointer = ref;
229         if (schema.getId() != null && ref.startsWith(schema.getId())) {
230             String refValue = ref.substring(schema.getId().length());
231             jsonPointer = refValue;
232         }
233         if (jsonPointer.startsWith("#/")) {
234             jsonPointer = jsonPointer.substring(1);
235         }
236 
237         if (jsonPointer.startsWith("/")) {
238             try {
239                 jsonPointer = URLDecoder.decode(jsonPointer, "utf-8");
240             } catch (UnsupportedEncodingException e) {
241                 // ignored
242             }
243 
244             node = node.at(jsonPointer);
245             if (node.isMissingNode()) {
246                 node = handleNullNode(ref, schema);
247             }
248         }
249         return node;
250     }
251 
getRefSchema(JsonNodePath fragment)252     public JsonSchema getRefSchema(JsonNodePath fragment) {
253         if (PathType.JSON_POINTER.equals(fragment.getPathType())) {
254             // Json Pointer
255             return getSubSchema(fragment);
256         } else {
257             // Anchor
258             String base = this.getSchemaLocation().getAbsoluteIri() != null ? this.schemaLocation.getAbsoluteIri().toString() : "";
259             String anchor = base + "#" + fragment.toString();
260             JsonSchema result = this.validationContext.getSchemaResources().get(anchor);
261             if (result == null) {
262                 result  = this.validationContext.getDynamicAnchors().get(anchor);
263             }
264             if (result == null) {
265                 throw new JsonSchemaException("Unable to find anchor "+anchor);
266             }
267             return result;
268         }
269     }
270 
271     /**
272      * Gets the sub schema given the json pointer fragment.
273      *
274      * @param fragment the json pointer fragment
275      * @return the schema
276      */
getSubSchema(JsonNodePath fragment)277     public JsonSchema getSubSchema(JsonNodePath fragment) {
278         JsonSchema document = findSchemaResourceRoot();
279         JsonSchema parent = document;
280         JsonSchema subSchema = null;
281         JsonNode parentNode = parent.getSchemaNode();
282         SchemaLocation schemaLocation = document.getSchemaLocation();
283         JsonNodePath evaluationPath = document.getEvaluationPath();
284         int nameCount = fragment.getNameCount();
285         for (int x = 0; x < nameCount; x++) {
286             /*
287              * The sub schema is created by iterating through the parents in order to
288              * maintain the lexical parent schema context.
289              *
290              * If this is created directly from the schema node pointed to by the json
291              * pointer, the lexical context is lost and this will affect $ref resolution due
292              * to $id changes in the lexical scope.
293              */
294             Object segment = fragment.getElement(x);
295             JsonNode subSchemaNode = getNode(parentNode, segment);
296             if (subSchemaNode != null) {
297                 if (segment instanceof Number) {
298                     int index = ((Number) segment).intValue();
299                     schemaLocation = schemaLocation.append(index);
300                     evaluationPath = evaluationPath.append(index);
301                 } else {
302                     schemaLocation = schemaLocation.append(segment.toString());
303                     evaluationPath = evaluationPath.append(segment.toString());
304                 }
305                 /*
306                  * The parent validation context is used to create as there can be changes in
307                  * $schema is later drafts which means the validation context can change.
308                  */
309                 // This may need a redesign see Issue 939 and 940
310                 String id = parent.getValidationContext().resolveSchemaId(subSchemaNode);
311 //                if (!("definitions".equals(segment.toString()) || "$defs".equals(segment.toString())
312 //                        )) {
313                 if (id != null || x == nameCount - 1) {
314                     subSchema = parent.getValidationContext().newSchema(schemaLocation, evaluationPath, subSchemaNode,
315                             parent);
316                     parent = subSchema;
317                     schemaLocation = subSchema.getSchemaLocation();
318                 }
319                 parentNode = subSchemaNode;
320             } else {
321                 /*
322                  * This means that the fragment wasn't found in the document.
323                  *
324                  * In Draft 4-7 the $id indicates a base uri change and not a schema resource so this might not be the right document.
325                  *
326                  * See test for draft4\extra\classpath\schema.json
327                  */
328                 JsonSchema found = document.findSchemaResourceRoot().fetchSubSchemaNode(this.validationContext);
329                 if (found != null) {
330                     found = found.getSubSchema(fragment);
331                 }
332                 if (found == null) {
333                     ValidationMessage validationMessage = ValidationMessage.builder()
334                             .type(ValidatorTypeCode.REF.getValue()).code("internal.unresolvedRef")
335                             .message("{0}: Reference {1} cannot be resolved")
336                             .instanceLocation(schemaLocation.getFragment())
337                             .schemaLocation(schemaLocation)
338                             .evaluationPath(evaluationPath)
339                             .arguments(fragment).build();
340                     throw new InvalidSchemaRefException(validationMessage);
341                 }
342                 return found;
343             }
344         }
345         return subSchema;
346     }
347 
getNode(Object propertyOrIndex)348     protected JsonNode getNode(Object propertyOrIndex) {
349         return getNode(this.schemaNode, propertyOrIndex);
350     }
351 
getNode(JsonNode node, Object propertyOrIndex)352     protected JsonNode getNode(JsonNode node, Object propertyOrIndex) {
353         return JsonNodes.get(node, propertyOrIndex);
354     }
355 
findLexicalRoot()356     public JsonSchema findLexicalRoot() {
357         JsonSchema ancestor = this;
358         while (ancestor.getId() == null) {
359             if (null == ancestor.getParentSchema()) break;
360             ancestor = ancestor.getParentSchema();
361         }
362         return ancestor;
363     }
364 
365     /**
366      * Finds the root of the schema resource.
367      * <p>
368      * This is either the schema document root or the subschema resource root.
369      *
370      * @return the root of the schema
371      */
findSchemaResourceRoot()372     public JsonSchema findSchemaResourceRoot() {
373         JsonSchema ancestor = this;
374         while (!ancestor.isSchemaResourceRoot()) {
375             ancestor = ancestor.getParentSchema();
376         }
377         return ancestor;
378     }
379 
380     /**
381      * Determines if this schema resource is a schema resource root.
382      * <p>
383      * This is either the schema document root or the subschema resource root.
384      *
385      * @return if this schema is a schema resource root
386      */
isSchemaResourceRoot()387     public boolean isSchemaResourceRoot() {
388         if (getId() != null) {
389             return true;
390         }
391         if (getParentSchema() == null) {
392             return true;
393         }
394         // The schema should not cross
395         if (!Objects.equals(getSchemaLocation().getAbsoluteIri(),
396                 getParentSchema().getSchemaLocation().getAbsoluteIri())) {
397             return true;
398         }
399         return false;
400     }
401 
getId()402     public String getId() {
403         return this.id;
404     }
405 
findAncestor()406     public JsonSchema findAncestor() {
407         JsonSchema ancestor = this;
408         if (this.getParentSchema() != null) {
409             ancestor = this.getParentSchema().findAncestor();
410         }
411         return ancestor;
412     }
413 
handleNullNode(String ref, JsonSchema schema)414     private JsonNode handleNullNode(String ref, JsonSchema schema) {
415         JsonSchema subSchema = schema.fetchSubSchemaNode(this.validationContext);
416         if (subSchema != null) {
417             return subSchema.getRefSchemaNode(ref);
418         }
419         return null;
420     }
421 
422     /**
423      * Please note that the key in {@link #validators} map is the evaluation path.
424      */
read(JsonNode schemaNode)425     private List<JsonValidator> read(JsonNode schemaNode) {
426         List<JsonValidator> validators = new ArrayList<>();
427         if (schemaNode.isBoolean()) {
428             if (schemaNode.booleanValue()) {
429                 JsonNodePath path = getEvaluationPath().append("true");
430                 JsonValidator validator = this.validationContext.newValidator(getSchemaLocation().append("true"), path,
431                         "true", schemaNode, this);
432                 validators.add(validator);
433             } else {
434                 JsonNodePath path = getEvaluationPath().append("false");
435                 JsonValidator validator = this.validationContext.newValidator(getSchemaLocation().append("false"),
436                         path, "false", schemaNode, this);
437                 validators.add(validator);
438             }
439         } else {
440             JsonValidator refValidator = null;
441 
442             Iterator<Entry<String, JsonNode>> iterator = schemaNode.fields();
443             while (iterator.hasNext()) {
444                 Entry<String, JsonNode> entry = iterator.next();
445                 String pname = entry.getKey();
446                 JsonNode nodeToUse = entry.getValue();
447 
448                 JsonNodePath path = getEvaluationPath().append(pname);
449                 SchemaLocation schemaPath = getSchemaLocation().append(pname);
450 
451                 if ("$recursiveAnchor".equals(pname)) {
452                     if (!nodeToUse.isBoolean()) {
453                         ValidationMessage validationMessage = ValidationMessage.builder().type("$recursiveAnchor")
454                                 .code("internal.invalidRecursiveAnchor")
455                                 .message(
456                                         "{0}: The value of a $recursiveAnchor must be a Boolean literal but is {1}")
457                                 .instanceLocation(path)
458                                 .evaluationPath(path)
459                                 .schemaLocation(schemaPath)
460                                 .arguments(nodeToUse.getNodeType().toString())
461                                 .build();
462                         throw new JsonSchemaException(validationMessage);
463                     }
464                     this.recursiveAnchor = nodeToUse.booleanValue();
465                 }
466 
467                 JsonValidator validator = this.validationContext.newValidator(schemaPath, path,
468                         pname, nodeToUse, this);
469                 if (validator != null) {
470                     validators.add(validator);
471 
472                     if ("$ref".equals(pname)) {
473                         refValidator = validator;
474                     } else if ("required".equals(pname)) {
475                         this.requiredValidator = validator;
476                     } else if ("type".equals(pname)) {
477                         this.typeValidator = (TypeValidator) validator;
478                     }
479                 }
480 
481             }
482 
483             // Ignore siblings for older drafts
484             if (null != refValidator && activeDialect() < V201909_VALUE) {
485                 validators.clear();
486                 validators.add(refValidator);
487             }
488         }
489         if (validators.size() > 1) {
490             Collections.sort(validators, VALIDATOR_SORT);
491         }
492         return validators;
493     }
494 
activeDialect()495     private long activeDialect() {
496         return this.validationContext
497             .activeDialect()
498             .map(VersionFlag::getVersionFlagValue)
499             .orElse(Long.MAX_VALUE);
500     }
501 
502     /**
503      * A comparator that sorts validators, such that 'properties' comes before 'required',
504      * so that we can apply default values before validating required.
505      */
506     private static Comparator<JsonValidator> VALIDATOR_SORT = (lhs, rhs) -> {
507         String lhsName = lhs.getEvaluationPath().getName(-1);
508         String rhsName = rhs.getEvaluationPath().getName(-1);
509 
510         if (lhsName.equals(rhsName)) return 0;
511 
512         if (lhsName.equals("properties")) return -1;
513         if (rhsName.equals("properties")) return 1;
514         if (lhsName.equals("patternProperties")) return -1;
515         if (rhsName.equals("patternProperties")) return 1;
516         if (lhsName.equals("unevaluatedItems")) return 1;
517         if (rhsName.equals("unevaluatedItems")) return -1;
518         if (lhsName.equals("unevaluatedProperties")) return 1;
519         if (rhsName.equals("unevaluatedProperties")) return -1;
520 
521         return 0; // retain original schema definition order
522     };
523 
524     /************************ START OF VALIDATE METHODS **********************************/
525 
526     @Override
validate(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation)527     public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation) {
528         if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
529             ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator");
530             if (null != discriminator && null != executionContext.getCurrentDiscriminatorContext()) {
531                 executionContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaLocation,
532                         discriminator);
533             }
534         }
535 
536         SetView<ValidationMessage> errors = null;
537         // Set the walkEnabled and isValidationEnabled flag in internal validator state.
538         setValidatorState(executionContext, false, true);
539 
540         for (JsonValidator v : getValidators()) {
541             Set<ValidationMessage> results = null;
542 
543             try {
544                 results = v.validate(executionContext, jsonNode, rootNode, instanceLocation);
545             } finally {
546                 if (results != null && !results.isEmpty()) {
547                     if (errors == null) {
548                         errors = new SetView<>();
549                     }
550                     errors.union(results);
551                 }
552             }
553         }
554 
555         if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
556             ObjectNode discriminator = (ObjectNode) this.schemaNode.get("discriminator");
557             if (null != discriminator) {
558                 final DiscriminatorContext discriminatorContext = executionContext
559                         .getCurrentDiscriminatorContext();
560                 if (null != discriminatorContext) {
561                     final ObjectNode discriminatorToUse;
562                     final ObjectNode discriminatorFromContext = discriminatorContext
563                             .getDiscriminatorForPath(this.schemaLocation);
564                     if (null == discriminatorFromContext) {
565                         // register the current discriminator. This can only happen when the current context discriminator
566                         // was not registered via allOf. In that case we have a $ref to the schema with discriminator that gets
567                         // used for validation before allOf validation has kicked in
568                         discriminatorContext.registerDiscriminator(this.schemaLocation, discriminator);
569                         discriminatorToUse = discriminator;
570                     } else {
571                         discriminatorToUse = discriminatorFromContext;
572                     }
573 
574                     final String discriminatorPropertyName = discriminatorToUse.get("propertyName").asText();
575                     final JsonNode discriminatorNode = jsonNode.get(discriminatorPropertyName);
576                     final String discriminatorPropertyValue = discriminatorNode == null ? null
577                             : discriminatorNode.asText();
578                     checkDiscriminatorMatch(discriminatorContext, discriminatorToUse, discriminatorPropertyValue,
579                             this);
580                 }
581             }
582         }
583 
584         if (errors != null && !errors.isEmpty()) {
585             // Failed with assertion set result and drop all annotations from this schema
586             // and all subschemas
587             executionContext.getResults().setResult(instanceLocation, getSchemaLocation(), getEvaluationPath(), false);
588         }
589         return errors == null ? Collections.emptySet() : errors;
590     }
591 
592     /**
593      * Validate the given root JsonNode, starting at the root of the data path.
594      * <p>
595      * Note that since Draft 2019-09 by default format generates only annotations
596      * and not assertions.
597      * <p>
598      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
599      * the default.
600      *
601      * @param rootNode the root node
602      * @return A list of ValidationMessage if there is any validation error, or an
603      *         empty list if there is no error.
604      */
validate(JsonNode rootNode)605     public Set<ValidationMessage> validate(JsonNode rootNode) {
606         return validate(rootNode, OutputFormat.DEFAULT);
607     }
608 
609     /**
610      * Validate the given root JsonNode, starting at the root of the data path.
611      * <p>
612      * Note that since Draft 2019-09 by default format generates only annotations
613      * and not assertions.
614      * <p>
615      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
616      * the default.
617      *
618      * @param rootNode            the root node
619      * @param executionCustomizer the execution customizer
620      * @return the assertions
621      */
validate(JsonNode rootNode, ExecutionContextCustomizer executionCustomizer)622     public Set<ValidationMessage> validate(JsonNode rootNode, ExecutionContextCustomizer executionCustomizer) {
623         return validate(rootNode, OutputFormat.DEFAULT, executionCustomizer);
624     }
625 
626     /**
627      * Validate the given root JsonNode, starting at the root of the data path.
628      * <p>
629      * Note that since Draft 2019-09 by default format generates only annotations
630      * and not assertions.
631      * <p>
632      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
633      * the default.
634      *
635      * @param rootNode            the root node
636      * @param executionCustomizer the execution customizer
637      * @return the assertions
638      */
validate(JsonNode rootNode, Consumer<ExecutionContext> executionCustomizer)639     public Set<ValidationMessage> validate(JsonNode rootNode, Consumer<ExecutionContext> executionCustomizer) {
640         return validate(rootNode, OutputFormat.DEFAULT, executionCustomizer);
641     }
642 
643     /**
644      * Validates the given root JsonNode, starting at the root of the data path. The
645      * output will be formatted using the formatter specified.
646      * <p>
647      * Note that since Draft 2019-09 by default format generates only annotations
648      * and not assertions.
649      * <p>
650      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
651      * the default.
652      *
653      * @param <T>      the result type
654      * @param rootNode the root node
655      * @param format   the formatter
656      * @return the result
657      */
validate(JsonNode rootNode, OutputFormat<T> format)658     public <T> T validate(JsonNode rootNode, OutputFormat<T> format) {
659         return validate(rootNode, format, (ExecutionContextCustomizer) null);
660     }
661 
662     /**
663      * Validates the given root JsonNode, starting at the root of the data path. The
664      * output will be formatted using the formatter specified.
665      * <p>
666      * Note that since Draft 2019-09 by default format generates only annotations
667      * and not assertions.
668      * <p>
669      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
670      * the default.
671      *
672      * @param <T>                 the result type
673      * @param rootNode            the root node
674      * @param format              the formatter
675      * @param executionCustomizer the execution customizer
676      * @return the result
677      */
validate(JsonNode rootNode, OutputFormat<T> format, ExecutionContextCustomizer executionCustomizer)678     public <T> T validate(JsonNode rootNode, OutputFormat<T> format, ExecutionContextCustomizer executionCustomizer) {
679         return validate(createExecutionContext(), rootNode, format, executionCustomizer);
680     }
681 
682     /**
683      * Validates the given root JsonNode, starting at the root of the data path. The
684      * output will be formatted using the formatter specified.
685      * <p>
686      * Note that since Draft 2019-09 by default format generates only annotations
687      * and not assertions.
688      * <p>
689      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
690      * the default.
691      *
692      * @param <T>                 the result type
693      * @param rootNode            the root node
694      * @param format              the formatter
695      * @param executionCustomizer the execution customizer
696      * @return the result
697      */
validate(JsonNode rootNode, OutputFormat<T> format, Consumer<ExecutionContext> executionCustomizer)698     public <T> T validate(JsonNode rootNode, OutputFormat<T> format, Consumer<ExecutionContext> executionCustomizer) {
699         return validate(createExecutionContext(), rootNode, format, (executionContext, validationContext) -> {
700             executionCustomizer.accept(executionContext);
701         });
702     }
703 
704     /**
705      * Validate the given input string using the input format, starting at the root
706      * of the data path.
707      * <p>
708      * Note that since Draft 2019-09 by default format generates only annotations
709      * and not assertions.
710      * <p>
711      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
712      * the default.
713      *
714      * @param input       the input
715      * @param inputFormat the inputFormat
716      * @return A list of ValidationMessage if there is any validation error, or an
717      *         empty list if there is no error.
718      */
validate(String input, InputFormat inputFormat)719     public Set<ValidationMessage> validate(String input, InputFormat inputFormat) {
720         return validate(deserialize(input, inputFormat), OutputFormat.DEFAULT);
721     }
722 
723     /**
724      * Validate the given input string using the input format, starting at the root
725      * of the data path.
726      * <p>
727      * Note that since Draft 2019-09 by default format generates only annotations
728      * and not assertions.
729      * <p>
730      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
731      * the default.
732      *
733      * @param input               the input
734      * @param inputFormat         the inputFormat
735      * @param executionCustomizer the execution customizer
736      * @return the assertions
737      */
validate(String input, InputFormat inputFormat, ExecutionContextCustomizer executionCustomizer)738     public Set<ValidationMessage> validate(String input, InputFormat inputFormat, ExecutionContextCustomizer executionCustomizer) {
739         return validate(deserialize(input, inputFormat), OutputFormat.DEFAULT, executionCustomizer);
740     }
741 
742     /**
743      * Validate the given input string using the input format, starting at the root
744      * of the data path.
745      * <p>
746      * Note that since Draft 2019-09 by default format generates only annotations
747      * and not assertions.
748      * <p>
749      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
750      * the default.
751      *
752      * @param input               the input
753      * @param inputFormat         the inputFormat
754      * @param executionCustomizer the execution customizer
755      * @return the assertions
756      */
validate(String input, InputFormat inputFormat, Consumer<ExecutionContext> executionCustomizer)757     public Set<ValidationMessage> validate(String input, InputFormat inputFormat, Consumer<ExecutionContext> executionCustomizer) {
758         return validate(deserialize(input, inputFormat), OutputFormat.DEFAULT, executionCustomizer);
759     }
760 
761     /**
762      * Validates the given input string using the input format, starting at the root
763      * of the data path. The output will be formatted using the formatter specified.
764      * <p>
765      * Note that since Draft 2019-09 by default format generates only annotations
766      * and not assertions.
767      * <p>
768      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
769      * the default.
770      *
771      * @param <T>         the result type
772      * @param input       the input
773      * @param inputFormat the inputFormat
774      * @param format      the formatter
775      * @return the result
776      */
validate(String input, InputFormat inputFormat, OutputFormat<T> format)777     public <T> T validate(String input, InputFormat inputFormat, OutputFormat<T> format) {
778         return validate(deserialize(input, inputFormat), format, (ExecutionContextCustomizer) null);
779     }
780 
781     /**
782      * Validates the given input string using the input format, starting at the root
783      * of the data path. The output will be formatted using the formatter specified.
784      * <p>
785      * Note that since Draft 2019-09 by default format generates only annotations
786      * and not assertions.
787      * <p>
788      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
789      * the default.
790      *
791      * @param <T>                 the result type
792      * @param input               the input
793      * @param inputFormat         the inputFormat
794      * @param format              the formatter
795      * @param executionCustomizer the execution customizer
796      * @return the result
797      */
validate(String input, InputFormat inputFormat, OutputFormat<T> format, ExecutionContextCustomizer executionCustomizer)798     public <T> T validate(String input, InputFormat inputFormat, OutputFormat<T> format, ExecutionContextCustomizer executionCustomizer) {
799         return validate(createExecutionContext(), deserialize(input, inputFormat), format, executionCustomizer);
800     }
801 
802     /**
803      * Validates the given input string using the input format, starting at the root
804      * of the data path. The output will be formatted using the formatter specified.
805      * <p>
806      * Note that since Draft 2019-09 by default format generates only annotations
807      * and not assertions.
808      * <p>
809      * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override
810      * the default.
811      *
812      * @param <T>                 the result type
813      * @param input               the input
814      * @param inputFormat         the inputFormat
815      * @param format              the formatter
816      * @param executionCustomizer the execution customizer
817      * @return the result
818      */
validate(String input, InputFormat inputFormat, OutputFormat<T> format, Consumer<ExecutionContext> executionCustomizer)819     public <T> T validate(String input, InputFormat inputFormat, OutputFormat<T> format, Consumer<ExecutionContext> executionCustomizer) {
820         return validate(createExecutionContext(), deserialize(input, inputFormat), format, (executionContext, validationContext) -> {
821             executionCustomizer.accept(executionContext);
822         });
823     }
824 
825     /**
826      * Validates to a format.
827      *
828      * @param <T>              the result type
829      * @param executionContext the execution context
830      * @param node             the node
831      * @param format           the format
832      * @return the result
833      */
834     public <T> T validate(ExecutionContext executionContext, JsonNode node, OutputFormat<T> format) {
835         return validate(executionContext, node, format, null);
836     }
837 
838     /**
839      * Validates to a format.
840      *
841      * @param <T>                 the result type
842      * @param executionContext    the execution context
843      * @param node                the node
844      * @param format              the format
845      * @param executionCustomizer the customizer
846      * @return the result
847      */
848     public <T> T validate(ExecutionContext executionContext, JsonNode node, OutputFormat<T> format,
849             ExecutionContextCustomizer executionCustomizer) {
850         format.customize(executionContext, this.validationContext);
851         if (executionCustomizer != null) {
852             executionCustomizer.customize(executionContext, this.validationContext);
853         }
854         Set<ValidationMessage> validationMessages = null;
855         try {
856             validationMessages = validate(executionContext, node);
857         } catch (FailFastAssertionException e) {
858             validationMessages = e.getValidationMessages();
859         }
860         return format.format(this, validationMessages, executionContext, this.validationContext);
861     }
862 
863     /**
864      * Deserialize string to JsonNode.
865      *
866      * @param input the input
867      * @param inputFormat the format
868      * @return the JsonNode.
869      */
870     private JsonNode deserialize(String input, InputFormat inputFormat) {
871         try {
872             if (InputFormat.JSON.equals(inputFormat)) {
873                 return JsonMapperFactory.getInstance().readTree(input);
874             } else if (InputFormat.YAML.equals(inputFormat)) {
875                 return YamlMapperFactory.getInstance().readTree(input);
876             }
877         } catch (JsonProcessingException e) {
878             throw new IllegalArgumentException("Invalid input", e);
879         }
880         throw new IllegalArgumentException("Unsupported input format "+inputFormat);
881     }
882 
883     public ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode node) {
884         return validateAndCollect(executionContext, node, node, atRoot());
885     }
886 
887     /**
888      * This method both validates and collects the data in a CollectorContext.
889      * Unlike others this methods cleans and removes everything from collector
890      * context before returning.
891      * @param executionContext ExecutionContext
892      * @param jsonNode JsonNode
893      * @param rootNode JsonNode
894      * @param instanceLocation JsonNodePath
895      *
896      * @return ValidationResult
897      */
898     private ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation) {
899         // Set the walkEnabled and isValidationEnabled flag in internal validator state.
900         setValidatorState(executionContext, false, true);
901         // Validate.
902         Set<ValidationMessage> errors = validate(executionContext, jsonNode, rootNode, instanceLocation);
903 
904         // Get the config.
905         SchemaValidatorsConfig config = this.validationContext.getConfig();
906 
907         // When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors.
908         if (config.doLoadCollectors()) {
909             // Get the collector context.
910             CollectorContext collectorContext = executionContext.getCollectorContext();
911 
912             // Load all the data from collectors into the context.
913             collectorContext.loadCollectors();
914         }
915         // Collect errors and collector context into validation result.
916         return new ValidationResult(errors, executionContext);
917     }
918 
919     public ValidationResult validateAndCollect(JsonNode node) {
920         return validateAndCollect(createExecutionContext(), node, node, atRoot());
921     }
922 
923     /************************ END OF VALIDATE METHODS **********************************/
924 
925     /*********************** START OF WALK METHODS **********************************/
926 
927     /**
928      * Walk the JSON node.
929      *
930      * @param executionContext the execution context
931      * @param node             the input
932      * @param validate         true to validate the input against the schema
933      *
934      * @return the validation result
935      */
936     public ValidationResult walk(ExecutionContext executionContext, JsonNode node, boolean validate) {
937         return walkAtNodeInternal(executionContext, node, node, atRoot(), validate);
938     }
939 
940     /**
941      * Walk the input.
942      *
943      * @param executionContext the execution context
944      * @param input            the input
945      * @param inputFormat      the input format
946      * @param validate         true to validate the input against the schema
947      * @return the validation result
948      */
949     public ValidationResult walk(ExecutionContext executionContext, String input, InputFormat inputFormat,
950             boolean validate) {
951         JsonNode node = deserialize(input, inputFormat);
952         return walkAtNodeInternal(executionContext, node, node, atRoot(), validate);
953     }
954 
955     /**
956      * Walk the JSON node.
957      *
958      * @param node     the input
959      * @param validate true to validate the input against the schema
960      * @return the validation result
961      */
962     public ValidationResult walk(JsonNode node, boolean validate) {
963         return walk(createExecutionContext(), node, validate);
964     }
965 
966     /**
967      * Walk the input.
968      *
969      * @param input       the input
970      * @param inputFormat the input format
971      * @param validate    true to validate the input against the schema
972      * @return the validation result
973      */
974     public ValidationResult walk(String input, InputFormat inputFormat, boolean validate) {
975         return walk(createExecutionContext(), deserialize(input, inputFormat), validate);
976     }
977 
978     /**
979      * Walk at the node.
980      *
981      * @param executionContext the execution content
982      * @param node             the current node
983      * @param rootNode         the root node
984      * @param instanceLocation the instance location
985      * @param validate         true to validate the input against the schema
986      * @return the validation result
987      */
988     public ValidationResult walkAtNode(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
989             JsonNodePath instanceLocation, boolean validate) {
990         return walkAtNodeInternal(executionContext, node, rootNode, instanceLocation, validate);
991     }
992 
993     private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
994             JsonNodePath instanceLocation, boolean shouldValidateSchema) {
995         // Set the walkEnabled flag in internal validator state.
996         setValidatorState(executionContext, true, shouldValidateSchema);
997         // Walk through the schema.
998         Set<ValidationMessage> errors = walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
999 
1000         // Get the config.
1001         SchemaValidatorsConfig config = this.validationContext.getConfig();
1002         // When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors.
1003         if (config.doLoadCollectors()) {
1004             // Get the collector context.
1005             CollectorContext collectorContext = executionContext.getCollectorContext();
1006 
1007             // Load all the data from collectors into the context.
1008             collectorContext.loadCollectors();
1009         }
1010         return new ValidationResult(errors, executionContext);
1011     }
1012 
1013     @Override
1014     public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
1015             JsonNodePath instanceLocation, boolean shouldValidateSchema) {
1016         Set<ValidationMessage> errors = new LinkedHashSet<>();
1017         // Walk through all the JSONWalker's.
1018         for (JsonValidator validator : getValidators()) {
1019             JsonNodePath evaluationPathWithKeyword = validator.getEvaluationPath();
1020             try {
1021                 // Call all the pre-walk listeners. If at least one of the pre walk listeners
1022                 // returns SKIP, then skip the walk.
1023                 if (this.validationContext.getConfig().getKeywordWalkListenerRunner().runPreWalkListeners(executionContext,
1024                         evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation,
1025                         this, validator)) {
1026                     Set<ValidationMessage> results = null;
1027                     try {
1028                         results = validator.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
1029                     } finally {
1030                         if (results != null && !results.isEmpty()) {
1031                             errors.addAll(results);
1032                         }
1033                     }
1034                 }
1035             } finally {
1036                 // Call all the post-walk listeners.
1037                 this.validationContext.getConfig().getKeywordWalkListenerRunner().runPostWalkListeners(executionContext,
1038                         evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation,
1039                         this, validator, errors);
1040             }
1041         }
1042         return errors;
1043     }
1044 
1045     /************************ END OF WALK METHODS **********************************/
1046 
1047     private static void setValidatorState(ExecutionContext executionContext, boolean isWalkEnabled,
1048             boolean shouldValidateSchema) {
1049         // Get the Validator state object storing validation data
1050         ValidatorState validatorState = executionContext.getValidatorState();
1051         if (validatorState == null) {
1052             // If one has not been created, instantiate one
1053             executionContext.setValidatorState(new ValidatorState(isWalkEnabled, shouldValidateSchema));
1054         }
1055     }
1056 
1057     @Override
1058     public String toString() {
1059         return "\"" + getEvaluationPath() + "\" : " + getSchemaNode().toString();
1060     }
1061 
1062     public boolean hasRequiredValidator() {
1063         return this.requiredValidator != null;
1064     }
1065 
1066     public JsonValidator getRequiredValidator() {
1067         return this.requiredValidator;
1068     }
1069 
1070     public boolean hasTypeValidator() {
1071         return getTypeValidator() != null;
1072     }
1073 
1074     public TypeValidator getTypeValidator() {
1075         // As the validators are lazy loaded the typeValidator is only known if the
1076         // validators are not null
1077         if (this.validators == null) {
1078             getValidators();
1079         }
1080         return this.typeValidator;
1081     }
1082 
1083     public List<JsonValidator> getValidators() {
1084         if (this.validators == null) {
1085             this.validators = Collections.unmodifiableList(read(getSchemaNode()));
1086         }
1087         return this.validators;
1088     }
1089 
1090     /**
1091      * Initializes the validators' {@link com.networknt.schema.JsonSchema} instances.
1092      * For avoiding issues with concurrency, in 1.0.49 the {@link com.networknt.schema.JsonSchema} instances affiliated with
1093      * validators were modified to no more preload the schema and lazy loading is used instead.
1094      * <p>This comes with the issue that this way you cannot rely on validating important schema features, in particular
1095      * <code>$ref</code> resolution at instantiation from {@link com.networknt.schema.JsonSchemaFactory}.</p>
1096      * <p>By calling <code>initializeValidators</code> you can enforce preloading of the {@link com.networknt.schema.JsonSchema}
1097      * instances of the validators.</p>
1098      */
1099     public void initializeValidators() {
1100         if (!this.validatorsLoaded) {
1101             for (final JsonValidator validator : getValidators()) {
1102                 validator.preloadJsonSchema();
1103             }
1104             /*
1105              * This is only set to true after the preload as it may throw an exception for
1106              * instance if the remote host is unavailable and we may want to be able to try
1107              * again.
1108              */
1109             this.validatorsLoaded = true;
1110         }
1111     }
1112 
1113     public boolean isRecursiveAnchor() {
1114         return this.recursiveAnchor;
1115     }
1116 
1117     /**
1118      * Creates an execution context.
1119      *
1120      * @return the execution context
1121      */
1122     public ExecutionContext createExecutionContext() {
1123         SchemaValidatorsConfig config = validationContext.getConfig();
1124         // Copy execution config defaults from validation config
1125         ExecutionConfig executionConfig = new ExecutionConfig();
1126         executionConfig.setLocale(config.getLocale());
1127         executionConfig.setFormatAssertionsEnabled(config.getFormatAssertionsEnabled());
1128         executionConfig.setFailFast(config.isFailFast());
1129 
1130         ExecutionContext executionContext = new ExecutionContext(executionConfig);
1131         if(config.getExecutionContextCustomizer() != null) {
1132             config.getExecutionContextCustomizer().customize(executionContext, validationContext);
1133         }
1134         return executionContext;
1135     }
1136 }
1137