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