From 459e85e16d8627534e4bce37afa19167343d253e Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:23:10 +0800 Subject: [PATCH 01/16] Add builder for SchemaValidatorsConfig. --- .../schema/SchemaValidatorsConfig.java | 1367 ++++++++++++----- 1 file changed, 962 insertions(+), 405 deletions(-) diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 75e5ff87c..1dc3ee0a5 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -1,7 +1,7 @@ /* * Copyright (c) 2016 Network New Technologies Inc. * - * Licensed under the Apache License, Version 2.0 (the "License"); + * Licensed under the Apache LicenseBuilder Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * @@ -39,18 +39,29 @@ * Configuration for validators. */ public class SchemaValidatorsConfig { + // This is just a constant for listening to all Keywords. + public static final String ALL_KEYWORD_WALK_LISTENER_KEY = "com.networknt.AllKeywordWalkListener"; + public static final int DEFAULT_PRELOAD_JSON_SCHEMA_REF_MAX_NESTING_DEPTH = 40; /** - * Used to validate the acceptable $id values. + * The strategy the walker uses to sets nodes that are missing or NullNode to + * the default value, if any, and mutate the input json. */ - private JsonSchemaIdValidator schemaIdValidator = JsonSchemaIdValidator.DEFAULT; + private ApplyDefaultsStrategy applyDefaultsStrategy = ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; /** - * when validate type, if TYPE_LOOSE = true, will try to convert string to - * different types to match the type defined in schema. + * Controls if schemas loaded from refs will be cached and reused for subsequent runs. */ - private boolean typeLoose; + private boolean cacheRefs = true; + + /** + * When set to true, "messages" provided in schema are used for forming validation errors + * else default messages are used + */ + private boolean customMessageSupported = true; + + private ExecutionContextCustomizer executionContextCustomizer; /** * When set to true, validator process is stop immediately when a very first @@ -59,15 +70,25 @@ public class SchemaValidatorsConfig { private boolean failFast; /** - * When set to true, walker sets nodes that are missing or NullNode to the - * default value, if any, and mutate the input json. + * Since Draft 2019-09 format assertions are not enabled by default. */ - private ApplyDefaultsStrategy applyDefaultsStrategy = ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; + private Boolean formatAssertionsEnabled = null; /** - * Used to create {@link com.networknt.schema.regex.RegularExpression}. + * When a field is set as nullable in the OpenAPI specification, the schema + * validator validates that it is nullable however continues with validation + * against the nullable field + *

+ * If handleNullableField is set to true && incoming field is nullable && value + * is field: null --> succeed If handleNullableField is set to false && incoming + * field is nullable && value is field: null --> it is up to the type validator + * using the SchemaValidator to handle it. */ - private RegularExpressionFactory regularExpressionFactory = JDKRegularExpressionFactory.getInstance(); + private boolean handleNullableField = true; + + private final WalkListenerRunner itemWalkListenerRunner = new DefaultItemWalkListenerRunner(getArrayItemWalkListeners()); + + private final List itemWalkListeners; /** * When set to true, use Java-specific semantics rather than native JavaScript @@ -75,16 +96,24 @@ public class SchemaValidatorsConfig { */ private boolean javaSemantics; + private final WalkListenerRunner keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(getKeywordWalkListenersMap()); + + private final Map> keywordWalkListenersMap; + + /** + * The Locale to consider when loading validation messages from the default resource bundle. + */ + private Locale locale; + /** * When set to true, can interpret round doubles as integers */ private boolean losslessNarrowing; /** - * When set to true, "messages" provided in schema are used for forming validation errors - * else default messages are used + * The message source to use for generating localised messages. */ - private boolean customMessageSupported = true; + private MessageSource messageSource; /** * When set to true, support for discriminators is enabled for validations of @@ -93,37 +122,6 @@ public class SchemaValidatorsConfig { */ private boolean openAPI3StyleDiscriminators = false; - /** - * Contains a mapping of how strict a keyword's validators should be. - * Defaults to {@literal true}. - *

- * Each validator has its own understanding of what constitutes strict - * and permissive. - */ - private final Map strictness = new HashMap<>(0); - - /** - * When a field is set as nullable in the OpenAPI specification, the schema - * validator validates that it is nullable however continues with validation - * against the nullable field - *

- * If handleNullableField is set to true && incoming field is nullable && value - * is field: null --> succeed If handleNullableField is set to false && incoming - * field is nullable && value is field: null --> it is up to the type validator - * using the SchemaValidator to handle it. - */ - private boolean handleNullableField = true; - - /** - * When set to true assumes that schema is used to validate incoming data from an API. - */ - private Boolean readOnly = null; - - /** - * When set to true assumes that schema is used to to validate outgoing data from an API. - */ - private Boolean writeOnly = null; - /** * The approach used to generate paths in reported messages, logs and errors. Default is the legacy "JSONPath-like" approach. */ @@ -139,163 +137,231 @@ public class SchemaValidatorsConfig { */ private int preloadJsonSchemaRefMaxNestingDepth = DEFAULT_PRELOAD_JSON_SCHEMA_REF_MAX_NESTING_DEPTH; - /** - * Controls if schemas loaded from refs will be cached and reused for subsequent runs. - */ - private boolean cacheRefs = true; - - // This is just a constant for listening to all Keywords. - public static final String ALL_KEYWORD_WALK_LISTENER_KEY = "com.networknt.AllKeywordWalkListener"; - - private final Map> keywordWalkListenersMap = new HashMap<>(); - - private final List propertyWalkListeners = new ArrayList<>(); - - private final List itemWalkListeners = new ArrayList<>(); + private final WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(getPropertyWalkListeners()); - private ExecutionContextCustomizer executionContextCustomizer; + private final List propertyWalkListeners; - @Deprecated - private boolean loadCollectors = true; + /** + * When set to true assumes that schema is used to validate incoming data from an API. + */ + private Boolean readOnly = null; /** - * The Locale to consider when loading validation messages from the default resource bundle. + * Used to create {@link com.networknt.schema.regex.RegularExpression}. */ - private Locale locale; + private RegularExpressionFactory regularExpressionFactory = JDKRegularExpressionFactory.getInstance(); /** - * The message source to use for generating localised messages. + * Used to validate the acceptable $id values. */ - private MessageSource messageSource; + private JsonSchemaIdValidator schemaIdValidator = JsonSchemaIdValidator.DEFAULT; + + /** + * Contains a mapping of how strict a keyword's validators should be. + * Defaults to {@literal true}. + *

+ * Each validator has its own understanding of what constitutes strict + * and permissive. + */ + private final Map strictness; /** - * Since Draft 2019-09 format assertions are not enabled by default. + * when validate type, if TYPE_LOOSE = true, will try to convert string to + * different types to match the type defined in schema. */ - private Boolean formatAssertionsEnabled = null; + private boolean typeLoose; - /************************ START OF UNEVALUATED CHECKS **********************************/ + /** + * When set to true assumes that schema is used to to validate outgoing data from an API. + */ + private Boolean writeOnly = null; @Deprecated - public SchemaValidatorsConfig disableUnevaluatedAnalysis() { - return this; + public SchemaValidatorsConfig() { + this.strictness = new HashMap<>(0); + + this.keywordWalkListenersMap = new HashMap<>(); + this.propertyWalkListeners = new ArrayList<>(); + this.itemWalkListeners = new ArrayList<>(); + } + + SchemaValidatorsConfig(ApplyDefaultsStrategy applyDefaultsStrategy, boolean cacheRefs, + boolean customMessageSupported, ExecutionContextCustomizer executionContextCustomizer, boolean failFast, + Boolean formatAssertionsEnabled, boolean handleNullableField, + List itemWalkListeners, boolean javaSemantics, + Map> keywordWalkListenersMap, Locale locale, boolean losslessNarrowing, + MessageSource messageSource, boolean openAPI3StyleDiscriminators, PathType pathType, + boolean preloadJsonSchema, int preloadJsonSchemaRefMaxNestingDepth, + List propertyWalkListeners, Boolean readOnly, + RegularExpressionFactory regularExpressionFactory, JsonSchemaIdValidator schemaIdValidator, + Map strictness, boolean typeLoose, Boolean writeOnly) { + super(); + this.applyDefaultsStrategy = applyDefaultsStrategy; + this.cacheRefs = cacheRefs; + this.customMessageSupported = customMessageSupported; + this.executionContextCustomizer = executionContextCustomizer; + this.failFast = failFast; + this.formatAssertionsEnabled = formatAssertionsEnabled; + this.handleNullableField = handleNullableField; + this.itemWalkListeners = itemWalkListeners; + this.javaSemantics = javaSemantics; + this.keywordWalkListenersMap = keywordWalkListenersMap; + this.locale = locale; + this.losslessNarrowing = losslessNarrowing; + this.messageSource = messageSource; + this.openAPI3StyleDiscriminators = openAPI3StyleDiscriminators; + this.pathType = pathType; + this.preloadJsonSchema = preloadJsonSchema; + this.preloadJsonSchemaRefMaxNestingDepth = preloadJsonSchemaRefMaxNestingDepth; + this.propertyWalkListeners = propertyWalkListeners; + this.readOnly = readOnly; + this.regularExpressionFactory = regularExpressionFactory; + this.schemaIdValidator = schemaIdValidator; + this.strictness = strictness; + this.typeLoose = typeLoose; + this.writeOnly = writeOnly; } - @Deprecated - public SchemaValidatorsConfig disableUnevaluatedItems() { - return this; + public void addItemWalkListener(JsonSchemaWalkListener itemWalkListener) { + this.itemWalkListeners.add(itemWalkListener); } - @Deprecated - public SchemaValidatorsConfig disableUnevaluatedProperties() { - return this; + public void addItemWalkListeners(List itemWalkListeners) { + this.itemWalkListeners.addAll(itemWalkListeners); } - @Deprecated - public SchemaValidatorsConfig enableUnevaluatedAnalysis() { - return this; + public void addKeywordWalkListener(JsonSchemaWalkListener keywordWalkListener) { + if (this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) { + List keywordWalkListeners = new ArrayList<>(); + this.keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, keywordWalkListeners); + } + this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).add(keywordWalkListener); } - @Deprecated - public SchemaValidatorsConfig enableUnevaluatedItems() { - return this; + public void addKeywordWalkListener(String keyword, JsonSchemaWalkListener keywordWalkListener) { + if (this.keywordWalkListenersMap.get(keyword) == null) { + List keywordWalkListeners = new ArrayList<>(); + this.keywordWalkListenersMap.put(keyword, keywordWalkListeners); + } + this.keywordWalkListenersMap.get(keyword).add(keywordWalkListener); } - @Deprecated - public SchemaValidatorsConfig enableUnevaluatedProperties() { - return this; + public void addKeywordWalkListeners(List keywordWalkListeners) { + if (this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) { + List ikeywordWalkListeners = new ArrayList<>(); + this.keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, ikeywordWalkListeners); + } + this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).addAll(keywordWalkListeners); } - @Deprecated - public boolean isUnevaluatedItemsAnalysisDisabled() { - return false; + public void addKeywordWalkListeners(String keyword, List keywordWalkListeners) { + if (this.keywordWalkListenersMap.get(keyword) == null) { + List ikeywordWalkListeners = new ArrayList<>(); + this.keywordWalkListenersMap.put(keyword, ikeywordWalkListeners); + } + this.keywordWalkListenersMap.get(keyword).addAll(keywordWalkListeners); } - @Deprecated - public boolean isUnevaluatedItemsAnalysisEnabled() { - return !isUnevaluatedItemsAnalysisDisabled(); + public void addPropertyWalkListener(JsonSchemaWalkListener propertyWalkListener) { + this.propertyWalkListeners.add(propertyWalkListener); } - @Deprecated - public boolean isUnevaluatedPropertiesAnalysisDisabled() { - return false; + public void addPropertyWalkListeners(List propertyWalkListeners) { + this.propertyWalkListeners.addAll(propertyWalkListeners); } - @Deprecated - public boolean isUnevaluatedPropertiesAnalysisEnabled() { - return !isUnevaluatedPropertiesAnalysisDisabled(); + public ApplyDefaultsStrategy getApplyDefaultsStrategy() { + return this.applyDefaultsStrategy; } - /************************ END OF UNEVALUATED CHECKS **********************************/ - - /** - * - * @return true if type loose is used. - */ - public boolean isTypeLoose() { - return this.typeLoose; + public List getArrayItemWalkListeners() { + return this.itemWalkListeners; } - public void setTypeLoose(boolean typeLoose) { - this.typeLoose = typeLoose; + public ExecutionContextCustomizer getExecutionContextCustomizer() { + return this.executionContextCustomizer; } /** - * When enabled, - * {@link JsonValidator#validate(ExecutionContext, JsonNode, JsonNode, JsonNodePath)} - * doesn't return any {@link java.util.Set}<{@link ValidationMessage}>, - * instead a {@link JsonSchemaException} is thrown as soon as a validation - * errors is discovered. - * - * @param failFast boolean + * Gets the format assertion enabled flag. + *

+ * This defaults to null meaning that it will follow the defaults of the + * specification. + *

+ * Since draft 2019-09 this will default to false unless enabled by using the + * $vocabulary keyword. + * + * @return the format assertions enabled flag */ - public void setFailFast(final boolean failFast) { - this.failFast = failFast; - } - - public boolean isFailFast() { - return this.failFast; + public Boolean getFormatAssertionsEnabled() { + return formatAssertionsEnabled; } - public void setApplyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy) { - this.applyDefaultsStrategy = applyDefaultsStrategy != null ? applyDefaultsStrategy - : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; + WalkListenerRunner getItemWalkListenerRunner() { + return this.itemWalkListenerRunner; } - public ApplyDefaultsStrategy getApplyDefaultsStrategy() { - return this.applyDefaultsStrategy; + WalkListenerRunner getKeywordWalkListenerRunner() { + return this.keywordWalkListenerRunner; } - public boolean isHandleNullableField() { - return this.handleNullableField; + public Map> getKeywordWalkListenersMap() { + return this.keywordWalkListenersMap; } - public void setHandleNullableField(boolean handleNullableField) { - this.handleNullableField = handleNullableField; + /** + * Get the locale to consider when generating localised messages (default is the + * JVM default). + *

+ * This locale is on a schema basis and will be used as the default locale for + * {@link com.networknt.schema.ExecutionConfig}. + * + * @return The locale. + */ + public Locale getLocale() { + if (this.locale == null) { + // This should not be cached as it can be changed using Locale#setDefault(Locale) + return Locale.getDefault(); + } + return this.locale; } /** - * Gets whether to use a ECMA-262 compliant regular expression validator. - *

- * This defaults to the false and setting true require inclusion of optional - * org.jruby.joni:joni or org.graalvm.js:js dependencies. + * Get the message source to use for generating localised messages. + * + * @return the message source + */ + public MessageSource getMessageSource() { + if (this.messageSource == null) { + return DefaultMessageSource.getInstance(); + } + return this.messageSource; + } + + /** + * Get the approach used to generate paths in messages, logs and errors. * - * @return true if ECMA-262 compliant + * @return The path generation approach. */ - public boolean isEcma262Validator() { - return !(this.regularExpressionFactory instanceof JDKRegularExpressionFactory); + public PathType getPathType() { + return this.pathType; } /** - * Sets whether to use a ECMA-262 compliant regular expression validator. - *

- * This defaults to the false and setting true require inclusion of optional - * org.jruby.joni:joni or org.graalvm.js:js dependencies. + * Gets the max depth of the evaluation path to preload when preloading refs. * - * @param ecma262Validator true if ECMA-262 compliant + * @return the max depth to preload */ - public void setEcma262Validator(boolean ecma262Validator) { - this.regularExpressionFactory = ecma262Validator ? ECMAScriptRegularExpressionFactory.getInstance() - : JDKRegularExpressionFactory.getInstance(); + public int getPreloadJsonSchemaRefMaxNestingDepth() { + return preloadJsonSchemaRefMaxNestingDepth; + } + + WalkListenerRunner getPropertyWalkListenerRunner() { + return this.propertyWalkListenerRunner; + } + + public List getPropertyWalkListeners() { + return this.propertyWalkListeners; } /** @@ -311,142 +377,214 @@ public RegularExpressionFactory getRegularExpressionFactory() { } /** - * Sets the regular expression factory. - *

- * This defaults to the JDKRegularExpressionFactory and the implementations - * require inclusion of optional org.jruby.joni:joni or org.graalvm.js:js dependencies. - * - * @see JDKRegularExpressionFactory - * @see ECMAScriptRegularExpressionFactory - * @param regularExpressionFactory the factory + * Gets the schema id validator to validate $id. + * + * @return the validator */ - public void setRegularExpressionFactory(RegularExpressionFactory regularExpressionFactory) { - this.regularExpressionFactory = regularExpressionFactory; - } - - public boolean isJavaSemantics() { - return this.javaSemantics; + public JsonSchemaIdValidator getSchemaIdValidator() { + return schemaIdValidator; } - public void setJavaSemantics(boolean javaSemantics) { - this.javaSemantics = javaSemantics; + /** + * Gets if schemas loaded from refs will be cached and reused for subsequent + * runs. + * + * @return true if schemas loaded from refs should be cached + */ + public boolean isCacheRefs() { + return cacheRefs; } public boolean isCustomMessageSupported() { return customMessageSupported; } - public void setCustomMessageSupported(boolean customMessageSupported) { - this.customMessageSupported = customMessageSupported; - } - - public void addKeywordWalkListener(JsonSchemaWalkListener keywordWalkListener) { - if (this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) { - List keywordWalkListeners = new ArrayList<>(); - this.keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, keywordWalkListeners); - } - this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).add(keywordWalkListener); + /** + * Gets whether to use a ECMA-262 compliant regular expression validator. + *

+ * This defaults to the false and setting true require inclusion of optional + * org.jruby.joni:joni or org.graalvm.js:js dependencies. + * + * @return true if ECMA-262 compliant + */ + public boolean isEcma262Validator() { + return !(this.regularExpressionFactory instanceof JDKRegularExpressionFactory); } - - public void addKeywordWalkListener(String keyword, JsonSchemaWalkListener keywordWalkListener) { - if (this.keywordWalkListenersMap.get(keyword) == null) { - List keywordWalkListeners = new ArrayList<>(); - this.keywordWalkListenersMap.put(keyword, keywordWalkListeners); - } - this.keywordWalkListenersMap.get(keyword).add(keywordWalkListener); + + public boolean isFailFast() { + return this.failFast; } - public void addKeywordWalkListeners(List keywordWalkListeners) { - if (this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) { - List ikeywordWalkListeners = new ArrayList<>(); - this.keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, ikeywordWalkListeners); - } - this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).addAll(keywordWalkListeners); + public boolean isHandleNullableField() { + return this.handleNullableField; } - public void addKeywordWalkListeners(String keyword, List keywordWalkListeners) { - if (this.keywordWalkListenersMap.get(keyword) == null) { - List ikeywordWalkListeners = new ArrayList<>(); - this.keywordWalkListenersMap.put(keyword, ikeywordWalkListeners); - } - this.keywordWalkListenersMap.get(keyword).addAll(keywordWalkListeners); + public boolean isJavaSemantics() { + return this.javaSemantics; } - public void addPropertyWalkListeners(List propertyWalkListeners) { - this.propertyWalkListeners.addAll(propertyWalkListeners); + public boolean isLosslessNarrowing() { + return this.losslessNarrowing; } - public void addPropertyWalkListener(JsonSchemaWalkListener propertyWalkListener) { - this.propertyWalkListeners.add(propertyWalkListener); + /** + * Indicates whether OpenAPI 3 style discriminators should be supported + * + * @return true in case discriminators are enabled + * @since 1.0.51 + */ + public boolean isOpenAPI3StyleDiscriminators() { + return this.openAPI3StyleDiscriminators; } - public void addItemWalkListener(JsonSchemaWalkListener itemWalkListener) { - this.itemWalkListeners.add(itemWalkListener); + /** + * Gets if the schema should be preloaded. + * + * @return true if it should be preloaded + */ + public boolean isPreloadJsonSchema() { + return preloadJsonSchema; } - public void addItemWalkListeners(List itemWalkListeners) { - this.itemWalkListeners.addAll(itemWalkListeners); + public boolean isReadOnly() { + return null != this.readOnly && this.readOnly; } - public List getPropertyWalkListeners() { - return this.propertyWalkListeners; + /** + * Answers whether a keyword's validators may relax their analysis. The + * default is to perform strict checking. One must explicitly allow a + * validator to be more permissive. + *

+ * Each validator has its own understanding of what is permissive and + * strict. Consult the keyword's documentation for details. + * + * @param keyword the keyword to adjust (not null) + * @return Whether to perform a strict validation. + */ + public boolean isStrict(String keyword) { + return isStrict(keyword, Boolean.TRUE); } - public Map> getKeywordWalkListenersMap() { - return this.keywordWalkListenersMap; + /** + * Determines if the validator should perform strict checking. + * + * @param keyword the keyword + * @param defaultValue the default value + * @return whether to perform a strict validation + */ + public boolean isStrict(String keyword, Boolean defaultValue) { + return this.strictness.getOrDefault(Objects.requireNonNull(keyword, "keyword cannot be null"), defaultValue); } - public List getArrayItemWalkListeners() { - return this.itemWalkListeners; + /** + * + * @return true if type loose is used. + */ + public boolean isTypeLoose() { + return this.typeLoose; } - private final WalkListenerRunner itemWalkListenerRunner = new DefaultItemWalkListenerRunner(getArrayItemWalkListeners()); - - WalkListenerRunner getItemWalkListenerRunner() { - return this.itemWalkListenerRunner; + public boolean isWriteOnly() { + return null != this.writeOnly && this.writeOnly; } - - private final WalkListenerRunner keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(getKeywordWalkListenersMap()); - WalkListenerRunner getKeywordWalkListenerRunner() { - return this.keywordWalkListenerRunner; + public void setApplyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy) { + this.applyDefaultsStrategy = applyDefaultsStrategy != null ? applyDefaultsStrategy + : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; } - private final WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(getPropertyWalkListeners()); - - WalkListenerRunner getPropertyWalkListenerRunner() { - return this.propertyWalkListenerRunner; + /** + * Sets if schemas loaded from refs will be cached and reused for subsequent + * runs. + *

+ * Note that setting this to false will affect performance as refs will need to + * be repeatedly resolved for each evaluation run. It may be needed to be set to + * false if there are multiple nested applicators like anyOf, oneOf and allOf as + * that will consume a lot of memory to cache all the permutations. + * + * @param cacheRefs true to cache + */ + public void setCacheRefs(boolean cacheRefs) { + this.cacheRefs = cacheRefs; } - public SchemaValidatorsConfig() { + public void setCustomMessageSupported(boolean customMessageSupported) { + this.customMessageSupported = customMessageSupported; } - public ExecutionContextCustomizer getExecutionContextCustomizer() { - return this.executionContextCustomizer; + /** + * Sets whether to use a ECMA-262 compliant regular expression validator. + *

+ * This defaults to the false and setting true require inclusion of optional + * org.jruby.joni:joni or org.graalvm.js:js dependencies. + * + * @param ecma262Validator true if ECMA-262 compliant + */ + public void setEcma262Validator(boolean ecma262Validator) { + this.regularExpressionFactory = ecma262Validator ? ECMAScriptRegularExpressionFactory.getInstance() + : JDKRegularExpressionFactory.getInstance(); } public void setExecutionContextCustomizer(ExecutionContextCustomizer executionContextCustomizer) { this.executionContextCustomizer = executionContextCustomizer; } - public boolean isLosslessNarrowing() { - return this.losslessNarrowing; + /** + * When enabled, + * {@link JsonValidator#validate(ExecutionContext, JsonNode, JsonNode, JsonNodePath)} + * doesn't return any {@link java.util.Set}<{@link ValidationMessage}>, + * instead a {@link JsonSchemaException} is thrown as soon as a validation + * errors is discovered. + * + * @param failFast boolean + */ + public void setFailFast(final boolean failFast) { + this.failFast = failFast; } - public void setLosslessNarrowing(boolean losslessNarrowing) { - this.losslessNarrowing = losslessNarrowing; + /** + * Sets the format assertion enabled flag. + * + * @param formatAssertionsEnabled the format assertions enabled flag + */ + public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { + this.formatAssertionsEnabled = formatAssertionsEnabled; + } + + public void setHandleNullableField(boolean handleNullableField) { + this.handleNullableField = handleNullableField; + } + + public void setJavaSemantics(boolean javaSemantics) { + this.javaSemantics = javaSemantics; } /** - * Indicates whether OpenAPI 3 style discriminators should be supported - * - * @return true in case discriminators are enabled - * @since 1.0.51 + * Set the locale to consider when generating localised messages. + *

+ * Note that this locale is set on a schema basis. To configure the schema on a + * per execution basis use + * {@link com.networknt.schema.ExecutionConfig#setLocale(Locale)}. + * + * @param locale The locale. */ - public boolean isOpenAPI3StyleDiscriminators() { - return this.openAPI3StyleDiscriminators; + public void setLocale(Locale locale) { + this.locale = locale; + } + + public void setLosslessNarrowing(boolean losslessNarrowing) { + this.losslessNarrowing = losslessNarrowing; } + /** + * Set the message source to use for generating localised messages. + * + * @param messageSource the message source + */ + public void setMessageSource(MessageSource messageSource) { + this.messageSource = messageSource; + } + /** * When enabled, the validation of anyOf and allOf in * polymorphism will respect OpenAPI 3 style discriminators as described in the @@ -485,94 +623,57 @@ public void setOpenAPI3StyleDiscriminators(boolean openAPI3StyleDiscriminators) } /** - * Sets if collectors are to be loaded. - *

- * This is deprecated in favor of the caller calling {@link CollectorContext#loadCollectors()} manually. - * - * @param loadCollectors to load collectors + * Set the approach used to generate paths in messages, logs and errors (default is PathType.LEGACY). + * + * @param pathType The path generation approach. */ - @Deprecated - public void setLoadCollectors(boolean loadCollectors) { - this.loadCollectors = loadCollectors; + public void setPathType(PathType pathType) { + this.pathType = pathType; } /** - * Gets if collectors are to be loaded. + * Sets if the schema should be preloaded. * - * @return if collectors are to be loader - */ - @Deprecated - public boolean doLoadCollectors() { - return this.loadCollectors; - } - - public boolean isReadOnly() { - return null != this.readOnly && this.readOnly; - } - - public void setReadOnly(boolean readOnly) { - this.readOnly = readOnly; - } - - public boolean isWriteOnly() { - return null != this.writeOnly && this.writeOnly; - } - - public void setWriteOnly(boolean writeOnly) { - this.writeOnly = writeOnly; - } - - /** - * Use {@code isReadOnly} or {@code isWriteOnly} - * @return true if schema is used to write data + * @param preloadJsonSchema true to preload */ - @Deprecated - public boolean isWriteMode() { - return null == this.writeOnly || this.writeOnly; + public void setPreloadJsonSchema(boolean preloadJsonSchema) { + this.preloadJsonSchema = preloadJsonSchema; } /** - * Set the approach used to generate paths in messages, logs and errors (default is PathType.LEGACY). + * Sets the max depth of the evaluation path to preload when preloading refs. * - * @param pathType The path generation approach. + * @param preloadJsonSchemaRefMaxNestingDepth the max depth to preload */ - public void setPathType(PathType pathType) { - this.pathType = pathType; + public void setPreloadJsonSchemaRefMaxNestingDepth(int preloadJsonSchemaRefMaxNestingDepth) { + this.preloadJsonSchemaRefMaxNestingDepth = preloadJsonSchemaRefMaxNestingDepth; } - /** - * Get the approach used to generate paths in messages, logs and errors. - * - * @return The path generation approach. - */ - public PathType getPathType() { - return this.pathType; + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; } /** - * Answers whether a keyword's validators may relax their analysis. The - * default is to perform strict checking. One must explicitly allow a - * validator to be more permissive. + * Sets the regular expression factory. *

- * Each validator has its own understanding of what is permissive and - * strict. Consult the keyword's documentation for details. - * - * @param keyword the keyword to adjust (not null) - * @return Whether to perform a strict validation. + * This defaults to the JDKRegularExpressionFactory and the implementations + * require inclusion of optional org.jruby.joni:joni or org.graalvm.js:js dependencies. + * + * @see JDKRegularExpressionFactory + * @see ECMAScriptRegularExpressionFactory + * @param regularExpressionFactory the factory */ - public boolean isStrict(String keyword) { - return isStrict(keyword, Boolean.TRUE); + public void setRegularExpressionFactory(RegularExpressionFactory regularExpressionFactory) { + this.regularExpressionFactory = regularExpressionFactory; } /** - * Determines if the validator should perform strict checking. - * - * @param keyword the keyword - * @param defaultValue the default value - * @return whether to perform a strict validation + * Sets the schema id validator to validate $id. + * + * @param schemaIdValidator the validator */ - public boolean isStrict(String keyword, Boolean defaultValue) { - return this.strictness.getOrDefault(Objects.requireNonNull(keyword, "keyword cannot be null"), defaultValue); + public void setSchemaIdValidator(JsonSchemaIdValidator schemaIdValidator) { + this.schemaIdValidator = schemaIdValidator; } /** @@ -587,157 +688,613 @@ public void setStrict(String keyword, boolean strict) { this.strictness.put(Objects.requireNonNull(keyword, "keyword cannot be null"), strict); } + public void setTypeLoose(boolean typeLoose) { + this.typeLoose = typeLoose; + } + + public void setWriteOnly(boolean writeOnly) { + this.writeOnly = writeOnly; + } + + public static Builder builder() { + return new Builder(); + } + /** - * Get the locale to consider when generating localised messages (default is the - * JVM default). - *

- * This locale is on a schema basis and will be used as the default locale for - * {@link com.networknt.schema.ExecutionConfig}. - * - * @return The locale. + * Builder for {@link SchemaValidatorsConfig}. */ - public Locale getLocale() { - if (this.locale == null) { - // This should not be cached as it can be changed using Locale#setDefault(Locale) - return Locale.getDefault(); + public static class Builder { + private ApplyDefaultsStrategy applyDefaultsStrategy = ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; + private boolean cacheRefs = true; + private boolean customMessageEnabled = false; + private ExecutionContextCustomizer executionContextCustomizer = null; + private boolean failFast = false; + private Boolean formatAssertionsEnabled = false; + private boolean handleNullableField = false; + private List itemWalkListeners = new ArrayList<>(); + private boolean javaSemantics = false; + private Map> keywordWalkListeners = new HashMap<>(); + private Locale locale = null; // This must be null to use Locale.getDefault() as the default can be changed + private boolean losslessNarrowing = false; + private MessageSource messageSource = null; + private boolean openAPI3StyleDiscriminators = false; + private PathType pathType = PathType.JSON_POINTER; + private boolean preloadJsonSchema = true; + private int preloadJsonSchemaRefMaxNestingDepth = DEFAULT_PRELOAD_JSON_SCHEMA_REF_MAX_NESTING_DEPTH; + private List propertyWalkListeners = new ArrayList<>(); + private Boolean readOnly = null; + private RegularExpressionFactory regularExpressionFactory = JDKRegularExpressionFactory.getInstance(); + private JsonSchemaIdValidator schemaIdValidator = JsonSchemaIdValidator.DEFAULT; + private Map strictness = new HashMap<>(0); + private boolean typeLoose = false; + private Boolean writeOnly = null; + + /** + * Sets the strategy the walker uses to sets nodes to the default value. + *

+ * Defaults to {@link ApplyDefaultsStrategy#EMPTY_APPLY_DEFAULTS_STRATEGY}. + * + * @param applyDefaultsStrategy the strategy + * @return the builder + */ + public Builder applyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy) { + this.applyDefaultsStrategy = applyDefaultsStrategy; + return this; + } + /** + * Sets if schemas loaded from refs will be cached and reused for subsequent runs. + *

+ * Defaults to true. + * + * @param cacheRefs true to cache + * @return the builder + */ + public Builder cacheRefs(boolean cacheRefs) { + this.cacheRefs = cacheRefs; + return this; + } + /** + * Sets if the message keyword for setting custom messages in the schema is enabled. + *

+ * Defaults to false. + * + * @param messageKeywordEnabled true to enable + * @return the builder + */ + public Builder messageKeywordEnabled(boolean messageKeywordEnabled) { + this.customMessageEnabled = messageKeywordEnabled; + return this; + } + /** + * Sets the execution context customizer that is run before each run. + * + * @param executionContextCustomizer the customizer + * @return the builder + */ + public Builder executionContextCustomizer(ExecutionContextCustomizer executionContextCustomizer) { + this.executionContextCustomizer = executionContextCustomizer; + return this; + } + + /** + * Sets if the validation should immediately return once a validation error has + * occurred. This can improve performance if inputs are invalid but cannot + * return all error messages to the caller. + *

+ * Defaults to false. + * + * @param failFast true to enable + * @return the builder + */ + public Builder failFast(boolean failFast) { + this.failFast = failFast; + return this; + } + + /** + * Sets if format assertions are enabled. If format assertions are not enabled + * the format keyword will behave like a annotation and not attempt to validate + * if the inputs are valid. + *

+ * Defaults to not enabling format assertions for Draft 2019-09 and above and + * enabling format assertions for Draft 7 and below. + * + * @param formatAssertionsEnabled true to enable + * @return the builder + */ + public Builder formatAssertionsEnabled(Boolean formatAssertionsEnabled) { + this.formatAssertionsEnabled = formatAssertionsEnabled; + return this; + } + + /** + * Sets if the nullable keyword is enabled. + * + * @param nullableKeywordEnabled true to enable + * @return the builder + */ + public Builder nullableKeywordEnabled(boolean nullableKeywordEnabled) { + this.handleNullableField = nullableKeywordEnabled; + return this; + } + public Builder itemWalkListeners(List itemWalkListeners) { + this.itemWalkListeners = itemWalkListeners; + return this; + } + public Builder javaSemantics(boolean javaSemantics) { + this.javaSemantics = javaSemantics; + return this; + } + public Builder keywordWalkListeners(Map> keywordWalkListeners) { + this.keywordWalkListeners = keywordWalkListeners; + return this; + } + /** + * Set the locale to consider when generating localised messages. + *

+ * Note that this locale is set on a schema basis. To configure the schema on a + * per execution basis use + * {@link com.networknt.schema.ExecutionConfig#setLocale(Locale)}. + *

+ * Defaults to use {@link Locale#getDefault()}. + * + * @param locale The locale. + */ + public Builder locale(Locale locale) { + this.locale = locale; + return this; + } + public Builder losslessNarrowing(boolean losslessNarrowing) { + this.losslessNarrowing = losslessNarrowing; + return this; + } + /** + * Sets the message source to use for generating localised messages. + * + * @param messageSource the message source + * @return the builder + */ + public Builder messageSource(MessageSource messageSource) { + this.messageSource = messageSource; + return this; + } + /** + * Sets if the discriminator keyword is enabled. + *

+ * Defaults to false. + * + * @param discriminatorKeywordEnabled true to enable + * @return the builder + */ + public Builder discriminatorKeywordEnabled(boolean discriminatorKeywordEnabled) { + this.openAPI3StyleDiscriminators = discriminatorKeywordEnabled; + return this; + } + /** + * Sets the path type to use when reporting the instance location of errors. + *

+ * Defaults to {@link PathType#JSON_POINTER}. + * + * @param pathType the path type + * @return the path type + */ + public Builder pathType(PathType pathType) { + this.pathType = pathType; + return this; + } + /** + * Sets if the schema should be preloaded. + *

+ * Defaults to true. + * + * @param preloadJsonSchema true to preload + * @return the builder + */ + public Builder preloadJsonSchema(boolean preloadJsonSchema) { + this.preloadJsonSchema = preloadJsonSchema; + return this; + } + /** + * Sets the max depth of the evaluation path to preload when preloading refs. + *

+ * Defaults to 40. + * + * @param preloadJsonSchemaRefMaxNestingDepth to preload + * @return the builder + */ + public Builder preloadJsonSchemaRefMaxNestingDepth(int preloadJsonSchemaRefMaxNestingDepth) { + this.preloadJsonSchemaRefMaxNestingDepth = preloadJsonSchemaRefMaxNestingDepth; + return this; + } + public Builder propertyWalkListeners(List propertyWalkListeners) { + this.propertyWalkListeners = propertyWalkListeners; + return this; + } + public Builder readOnly(Boolean readOnly) { + this.readOnly = readOnly; + return this; + } + /** + * Sets the regular expression factory. + *

+ * Defaults to the {@link JDKRegularExpressionFactory} + *

+ * The {@link ECMAScriptRegularExpressionFactory} requires the inclusion of + * optional org.jruby.joni:joni or org.graalvm.js:js dependencies. + * + * @see JDKRegularExpressionFactory + * @see ECMAScriptRegularExpressionFactory + * @param regularExpressionFactory the factory + * @return the builder + */ + public Builder regularExpressionFactory(RegularExpressionFactory regularExpressionFactory) { + this.regularExpressionFactory = regularExpressionFactory; + return this; + } + /** + * Sets the schema id validator to use. + *

+ * Defaults to {@link JsonSchemaIdValidator#DEFAULT}. + * + * @param schemaIdValidator the builder + * @return the builder + */ + public Builder schemaIdValidator(JsonSchemaIdValidator schemaIdValidator) { + this.schemaIdValidator = schemaIdValidator; + return this; + } + public Builder strict(Map strict) { + this.strictness = strict; + return this; + } + public Builder typeLoose(boolean typeLoose) { + this.typeLoose = typeLoose; + return this; + } + public Builder writeOnly(Boolean writeOnly) { + this.writeOnly = writeOnly; + return this; + } + public SchemaValidatorsConfig build() { + return new ImmutableSchemaValidatorsConfig(applyDefaultsStrategy, cacheRefs, customMessageEnabled, + executionContextCustomizer, failFast, formatAssertionsEnabled, handleNullableField, + itemWalkListeners, javaSemantics, keywordWalkListeners, locale, losslessNarrowing, messageSource, + openAPI3StyleDiscriminators, pathType, preloadJsonSchema, preloadJsonSchemaRefMaxNestingDepth, + propertyWalkListeners, readOnly, regularExpressionFactory, schemaIdValidator, strictness, typeLoose, + writeOnly); + } + public Builder strict(String keyword, boolean strict) { + this.strictness.put(Objects.requireNonNull(keyword, "keyword cannot be null"), strict); + return this; } - return this.locale; } /** - * Set the locale to consider when generating localised messages. + * {@link SchemaValidatorsConfig} that throws on mutators or deprecated methods. *

- * Note that this locale is set on a schema basis. To configure the schema on a - * per execution basis use - * {@link com.networknt.schema.ExecutionConfig#setLocale(Locale)}. - * - * @param locale The locale. + * The {@link SchemaValidatorsConfig} will be made immutable in a future breaking release. */ - public void setLocale(Locale locale) { - this.locale = locale; - } + public static class ImmutableSchemaValidatorsConfig extends SchemaValidatorsConfig { + public ImmutableSchemaValidatorsConfig(ApplyDefaultsStrategy applyDefaultsStrategy, boolean cacheRefs, + boolean customMessageSupported, ExecutionContextCustomizer executionContextCustomizer, boolean failFast, + Boolean formatAssertionsEnabled, boolean handleNullableField, + List itemWalkListeners, boolean javaSemantics, + Map> keywordWalkListenersMap, Locale locale, + boolean losslessNarrowing, MessageSource messageSource, boolean openAPI3StyleDiscriminators, + PathType pathType, boolean preloadJsonSchema, int preloadJsonSchemaRefMaxNestingDepth, + List propertyWalkListeners, Boolean readOnly, + RegularExpressionFactory regularExpressionFactory, JsonSchemaIdValidator schemaIdValidator, + Map strictness, boolean typeLoose, Boolean writeOnly) { + super(applyDefaultsStrategy, cacheRefs, customMessageSupported, executionContextCustomizer, failFast, + formatAssertionsEnabled, handleNullableField, itemWalkListeners, javaSemantics, keywordWalkListenersMap, locale, + losslessNarrowing, messageSource, openAPI3StyleDiscriminators, pathType, preloadJsonSchema, + preloadJsonSchemaRefMaxNestingDepth, propertyWalkListeners, readOnly, regularExpressionFactory, + schemaIdValidator, strictness, typeLoose, writeOnly); + } - /** - * Get the message source to use for generating localised messages. - * - * @return the message source - */ - public MessageSource getMessageSource() { - if (this.messageSource == null) { - return DefaultMessageSource.getInstance(); + @Override + public void addItemWalkListener(JsonSchemaWalkListener itemWalkListener) { + throw new UnsupportedOperationException(); + } + + @Override + public void addItemWalkListeners(List itemWalkListeners) { + throw new UnsupportedOperationException(); + } + + @Override + public void addKeywordWalkListener(JsonSchemaWalkListener keywordWalkListener) { + throw new UnsupportedOperationException(); + } + + @Override + public void addKeywordWalkListener(String keyword, JsonSchemaWalkListener keywordWalkListener) { + throw new UnsupportedOperationException(); + } + + @Override + public void addKeywordWalkListeners(List keywordWalkListeners) { + throw new UnsupportedOperationException(); + } + + @Override + public void addKeywordWalkListeners(String keyword, List keywordWalkListeners) { + throw new UnsupportedOperationException(); + } + + @Override + public void addPropertyWalkListener(JsonSchemaWalkListener propertyWalkListener) { + throw new UnsupportedOperationException(); + } + + @Override + public void addPropertyWalkListeners(List propertyWalkListeners) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isWriteOnly() { + throw new UnsupportedOperationException(); + } + + @Override + public void setApplyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy) { + throw new UnsupportedOperationException(); + } + + @Override + public void setCacheRefs(boolean cacheRefs) { + throw new UnsupportedOperationException(); + } + + @Override + public void setCustomMessageSupported(boolean customMessageSupported) { + throw new UnsupportedOperationException(); + } + + @Override + public void setEcma262Validator(boolean ecma262Validator) { + throw new UnsupportedOperationException(); + } + + @Override + public void setExecutionContextCustomizer(ExecutionContextCustomizer executionContextCustomizer) { + throw new UnsupportedOperationException(); + } + + @Override + public void setFailFast(boolean failFast) { + throw new UnsupportedOperationException(); + } + + @Override + public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { + throw new UnsupportedOperationException(); + } + + @Override + public void setHandleNullableField(boolean handleNullableField) { + throw new UnsupportedOperationException(); + } + + @Override + public void setJavaSemantics(boolean javaSemantics) { + throw new UnsupportedOperationException(); + } + + @Override + public void setLocale(Locale locale) { + throw new UnsupportedOperationException(); + } + + @Override + public void setLosslessNarrowing(boolean losslessNarrowing) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMessageSource(MessageSource messageSource) { + throw new UnsupportedOperationException(); + } + + @Override + public void setOpenAPI3StyleDiscriminators(boolean openAPI3StyleDiscriminators) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPathType(PathType pathType) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPreloadJsonSchema(boolean preloadJsonSchema) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPreloadJsonSchemaRefMaxNestingDepth(int preloadJsonSchemaRefMaxNestingDepth) { + throw new UnsupportedOperationException(); + } + + @Override + public void setReadOnly(boolean readOnly) { + throw new UnsupportedOperationException(); + } + + @Override + public void setRegularExpressionFactory(RegularExpressionFactory regularExpressionFactory) { + throw new UnsupportedOperationException(); + } + + @Override + public void setSchemaIdValidator(JsonSchemaIdValidator schemaIdValidator) { + throw new UnsupportedOperationException(); + } + + @Override + public void setStrict(String keyword, boolean strict) { + throw new UnsupportedOperationException(); + } + + @Override + public void setTypeLoose(boolean typeLoose) { + throw new UnsupportedOperationException(); + } + + @Override + public void setWriteOnly(boolean writeOnly) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isWriteMode() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLoadCollectors(boolean loadCollectors) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean doLoadCollectors() { + throw new UnsupportedOperationException(); + } + + @Override + public SchemaValidatorsConfig disableUnevaluatedAnalysis() { + throw new UnsupportedOperationException(); + } + + @Override + public SchemaValidatorsConfig disableUnevaluatedItems() { + throw new UnsupportedOperationException(); + } + + @Override + public SchemaValidatorsConfig disableUnevaluatedProperties() { + throw new UnsupportedOperationException(); + } + + @Override + public SchemaValidatorsConfig enableUnevaluatedAnalysis() { + throw new UnsupportedOperationException(); + } + + @Override + public SchemaValidatorsConfig enableUnevaluatedItems() { + throw new UnsupportedOperationException(); + } + + @Override + public SchemaValidatorsConfig enableUnevaluatedProperties() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUnevaluatedItemsAnalysisDisabled() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUnevaluatedItemsAnalysisEnabled() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUnevaluatedPropertiesAnalysisDisabled() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUnevaluatedPropertiesAnalysisEnabled() { + throw new UnsupportedOperationException(); } - return this.messageSource; } + /* Below are deprecated for removal */ + @Deprecated + private boolean loadCollectors = true; + /** - * Set the message source to use for generating localised messages. - * - * @param messageSource the message source + * Use {@code isReadOnly} or {@code isWriteOnly} + * @return true if schema is used to write data */ - public void setMessageSource(MessageSource messageSource) { - this.messageSource = messageSource; + @Deprecated + public boolean isWriteMode() { + return null == this.writeOnly || this.writeOnly; } /** - * Gets the format assertion enabled flag. - *

- * This defaults to null meaning that it will follow the defaults of the - * specification. + * Sets if collectors are to be loaded. *

- * Since draft 2019-09 this will default to false unless enabled by using the - * $vocabulary keyword. + * This is deprecated in favor of the caller calling {@link CollectorContext#loadCollectors()} manually. * - * @return the format assertions enabled flag + * @param loadCollectors to load collectors */ - public Boolean getFormatAssertionsEnabled() { - return formatAssertionsEnabled; + @Deprecated + public void setLoadCollectors(boolean loadCollectors) { + this.loadCollectors = loadCollectors; } /** - * Sets the format assertion enabled flag. + * Gets if collectors are to be loaded. * - * @param formatAssertionsEnabled the format assertions enabled flag + * @return if collectors are to be loader */ - public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { - this.formatAssertionsEnabled = formatAssertionsEnabled; + @Deprecated + public boolean doLoadCollectors() { + return this.loadCollectors; } - /** - * Gets the schema id validator to validate $id. - * - * @return the validator - */ - public JsonSchemaIdValidator getSchemaIdValidator() { - return schemaIdValidator; + @Deprecated + public SchemaValidatorsConfig disableUnevaluatedAnalysis() { + return this; } - /** - * Sets the schema id validator to validate $id. - * - * @param schemaIdValidator the validator - */ - public void setSchemaIdValidator(JsonSchemaIdValidator schemaIdValidator) { - this.schemaIdValidator = schemaIdValidator; + @Deprecated + public SchemaValidatorsConfig disableUnevaluatedItems() { + return this; } - /** - * Gets if the schema should be preloaded. - * - * @return true if it should be preloaded - */ - public boolean isPreloadJsonSchema() { - return preloadJsonSchema; + @Deprecated + public SchemaValidatorsConfig disableUnevaluatedProperties() { + return this; } - /** - * Sets if the schema should be preloaded. - * - * @param preloadJsonSchema true to preload - */ - public void setPreloadJsonSchema(boolean preloadJsonSchema) { - this.preloadJsonSchema = preloadJsonSchema; + @Deprecated + public SchemaValidatorsConfig enableUnevaluatedAnalysis() { + return this; } - /** - * Gets the max depth of the evaluation path to preload when preloading refs. - * - * @return the max depth to preload - */ - public int getPreloadJsonSchemaRefMaxNestingDepth() { - return preloadJsonSchemaRefMaxNestingDepth; + @Deprecated + public SchemaValidatorsConfig enableUnevaluatedItems() { + return this; } - /** - * Sets the max depth of the evaluation path to preload when preloading refs. - * - * @param preloadJsonSchemaRefMaxNestingDepth the max depth to preload - */ - public void setPreloadJsonSchemaRefMaxNestingDepth(int preloadJsonSchemaRefMaxNestingDepth) { - this.preloadJsonSchemaRefMaxNestingDepth = preloadJsonSchemaRefMaxNestingDepth; + @Deprecated + public SchemaValidatorsConfig enableUnevaluatedProperties() { + return this; } - /** - * Gets if schemas loaded from refs will be cached and reused for subsequent - * runs. - * - * @return true if schemas loaded from refs should be cached - */ - public boolean isCacheRefs() { - return cacheRefs; + @Deprecated + public boolean isUnevaluatedItemsAnalysisDisabled() { + return false; } - /** - * Sets if schemas loaded from refs will be cached and reused for subsequent - * runs. - *

- * Note that setting this to false will affect performance as refs will need to - * be repeatedly resolved for each evaluation run. It may be needed to be set to - * false if there are multiple nested applicators like anyOf, oneOf and allOf as - * that will consume a lot of memory to cache all the permutations. - * - * @param cacheRefs true to cache - */ - public void setCacheRefs(boolean cacheRefs) { - this.cacheRefs = cacheRefs; + @Deprecated + public boolean isUnevaluatedItemsAnalysisEnabled() { + return !isUnevaluatedItemsAnalysisDisabled(); + } + + @Deprecated + public boolean isUnevaluatedPropertiesAnalysisDisabled() { + return false; + } + + @Deprecated + public boolean isUnevaluatedPropertiesAnalysisEnabled() { + return !isUnevaluatedPropertiesAnalysisDisabled(); } } From cf5f59cc1f9a65aa7d3a19e4fe1b71e9b04fc4f5 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:42:20 +0800 Subject: [PATCH 02/16] Use an error message keyword instead of just a boolean --- .../networknt/schema/BaseJsonValidator.java | 10 +-- .../com/networknt/schema/JsonMetaSchema.java | 2 +- .../java/com/networknt/schema/JsonSchema.java | 10 +-- .../schema/SchemaValidatorsConfig.java | 67 ++++++++++++++----- .../schema/ValidationMessageHandler.java | 35 +++++----- 5 files changed, 80 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 87e9311a7..7c3ee1833 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -51,8 +51,8 @@ public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { super(errorMessageType, (validationContext != null && validationContext.getConfig() != null) - ? validationContext.getConfig().isCustomMessageSupported() - : true, + ? validationContext.getConfig().getErrorMessageKeyword() + : null, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().getMessageSource() : DefaultMessageSource.getInstance(), @@ -70,7 +70,7 @@ public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP * @param schemaNode the schema node * @param validationContext the validation context * @param errorMessageType the error message type - * @param customErrorMessagesEnabled whether custom error msessages are enabled + * @param errorMessageKeyword the error message keyword * @param messageSource the message source * @param keyword the keyword * @param parentSchema the parent schema @@ -86,7 +86,7 @@ protected BaseJsonValidator( ValidationContext validationContext, /* Below from ValidationMessageHandler */ ErrorMessageType errorMessageType, - boolean customErrorMessagesEnabled, + String errorMessageKeyword, MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, @@ -94,7 +94,7 @@ protected BaseJsonValidator( JsonNodePath evaluationPath, JsonSchema evaluationParentSchema, Map errorMessage) { - super(errorMessageType, customErrorMessagesEnabled, messageSource, keyword, + super(errorMessageType, errorMessageKeyword, messageSource, keyword, parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, errorMessage); this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; this.schemaNode = schemaNode; diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 4ae26bb4c..2cb451545 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -478,7 +478,7 @@ public JsonValidator newValidator(ValidationContext validationContext, SchemaLoc try { Keyword kw = this.keywords.get(keyword); if (kw == null) { - if ("message".equals(keyword) && validationContext.getConfig().isCustomMessageSupported()) { + if (keyword.equals(validationContext.getConfig().getErrorMessageKeyword())) { return null; } if (ValidatorTypeCode.DISCRIMINATOR.getValue().equals(keyword) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 1c3182f47..3b05a51db 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -162,7 +162,7 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc * @param schemaNode the schema node * @param validationContext the validation context * @param errorMessageType the error message type - * @param customErrorMessagesEnabled whether custom error msessages are enabled + * @param errorMessageKeyword the error message keyword * @param messageSource the message source * @param keyword the keyword * @param parentSchema the parent schema @@ -184,7 +184,7 @@ protected JsonSchema( ValidationContext validationContext, /* Below from ValidationMessageHandler */ ErrorMessageType errorMessageType, - boolean customErrorMessagesEnabled, + String errorMessageKeyword, MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, @@ -192,7 +192,7 @@ protected JsonSchema( JsonNodePath evaluationPath, JsonSchema evaluationParentSchema, Map errorMessage) { - super(suppressSubSchemaRetrieval, schemaNode, validationContext, errorMessageType, customErrorMessagesEnabled, messageSource, keyword, + super(suppressSubSchemaRetrieval, schemaNode, validationContext, errorMessageType, errorMessageKeyword, messageSource, keyword, parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, errorMessage); this.validators = validators; this.validatorsLoaded = validatorsLoaded; @@ -238,7 +238,7 @@ public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath ref schemaNode, validationContext, /* Below from ValidationMessageHandler */ - errorMessageType, customErrorMessagesEnabled, messageSource, + errorMessageType, errorMessageKeyword, messageSource, keyword, parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, errorMessage); } @@ -266,7 +266,7 @@ public JsonSchema withConfig(SchemaValidatorsConfig config) { validationContext, /* Below from ValidationMessageHandler */ errorMessageType, - customErrorMessagesEnabled, + errorMessageKeyword, messageSource, keyword, parentSchema, diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 1dc3ee0a5..495a42602 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -59,7 +59,7 @@ public class SchemaValidatorsConfig { * When set to true, "messages" provided in schema are used for forming validation errors * else default messages are used */ - private boolean customMessageSupported = true; + private String errorMessageKeyword = "message"; private ExecutionContextCustomizer executionContextCustomizer; @@ -86,7 +86,7 @@ public class SchemaValidatorsConfig { */ private boolean handleNullableField = true; - private final WalkListenerRunner itemWalkListenerRunner = new DefaultItemWalkListenerRunner(getArrayItemWalkListeners()); + private final WalkListenerRunner itemWalkListenerRunner; private final List itemWalkListeners; @@ -96,7 +96,7 @@ public class SchemaValidatorsConfig { */ private boolean javaSemantics; - private final WalkListenerRunner keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(getKeywordWalkListenersMap()); + private final WalkListenerRunner keywordWalkListenerRunner; private final Map> keywordWalkListenersMap; @@ -137,7 +137,7 @@ public class SchemaValidatorsConfig { */ private int preloadJsonSchemaRefMaxNestingDepth = DEFAULT_PRELOAD_JSON_SCHEMA_REF_MAX_NESTING_DEPTH; - private final WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(getPropertyWalkListeners()); + private final WalkListenerRunner propertyWalkListenerRunner; private final List propertyWalkListeners; @@ -176,6 +176,18 @@ public class SchemaValidatorsConfig { */ private Boolean writeOnly = null; + /** + * Constructor to create an instance. + *

+ * This is deprecated in favor of using the builder + * {@link SchemaValidatorsConfig#builder()} to create an instance. Migration + * note: The builder has different defaults from the constructor. + *

+ */ @Deprecated public SchemaValidatorsConfig() { this.strictness = new HashMap<>(0); @@ -183,10 +195,14 @@ public SchemaValidatorsConfig() { this.keywordWalkListenersMap = new HashMap<>(); this.propertyWalkListeners = new ArrayList<>(); this.itemWalkListeners = new ArrayList<>(); + + this.itemWalkListenerRunner = new DefaultItemWalkListenerRunner(getArrayItemWalkListeners()); + this.keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(getKeywordWalkListenersMap()); + this.propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(getPropertyWalkListeners()); } SchemaValidatorsConfig(ApplyDefaultsStrategy applyDefaultsStrategy, boolean cacheRefs, - boolean customMessageSupported, ExecutionContextCustomizer executionContextCustomizer, boolean failFast, + String errorMessageKeyword, ExecutionContextCustomizer executionContextCustomizer, boolean failFast, Boolean formatAssertionsEnabled, boolean handleNullableField, List itemWalkListeners, boolean javaSemantics, Map> keywordWalkListenersMap, Locale locale, boolean losslessNarrowing, @@ -198,7 +214,7 @@ public SchemaValidatorsConfig() { super(); this.applyDefaultsStrategy = applyDefaultsStrategy; this.cacheRefs = cacheRefs; - this.customMessageSupported = customMessageSupported; + this.errorMessageKeyword = errorMessageKeyword; this.executionContextCustomizer = executionContextCustomizer; this.failFast = failFast; this.formatAssertionsEnabled = formatAssertionsEnabled; @@ -220,6 +236,10 @@ public SchemaValidatorsConfig() { this.strictness = strictness; this.typeLoose = typeLoose; this.writeOnly = writeOnly; + + this.itemWalkListenerRunner = new DefaultItemWalkListenerRunner(getArrayItemWalkListeners()); + this.keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(getKeywordWalkListenersMap()); + this.propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(getPropertyWalkListeners()); } public void addItemWalkListener(JsonSchemaWalkListener itemWalkListener) { @@ -395,8 +415,13 @@ public boolean isCacheRefs() { return cacheRefs; } + @Deprecated public boolean isCustomMessageSupported() { - return customMessageSupported; + return this.errorMessageKeyword != null; + } + + public String getErrorMessageKeyword() { + return this.errorMessageKeyword; } /** @@ -508,8 +533,16 @@ public void setCacheRefs(boolean cacheRefs) { this.cacheRefs = cacheRefs; } + /** + * Sets whether custom error messages in the schema are used. + *

+ * This is deprecated in favor of setting the error message keyword to use. + * + * @param customMessageSupported true to use message as the error message keyword + */ + @Deprecated public void setCustomMessageSupported(boolean customMessageSupported) { - this.customMessageSupported = customMessageSupported; + this.errorMessageKeyword = customMessageSupported ? "message" : null; } /** @@ -706,7 +739,7 @@ public static Builder builder() { public static class Builder { private ApplyDefaultsStrategy applyDefaultsStrategy = ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; private boolean cacheRefs = true; - private boolean customMessageEnabled = false; + private String errorMessageKeyword = null; private ExecutionContextCustomizer executionContextCustomizer = null; private boolean failFast = false; private Boolean formatAssertionsEnabled = false; @@ -754,15 +787,15 @@ public Builder cacheRefs(boolean cacheRefs) { return this; } /** - * Sets if the message keyword for setting custom messages in the schema is enabled. + * Sets the error message keyword for setting custom messages in the schema. *

- * Defaults to false. + * Defaults to null meaning custom messages are not enabled. * - * @param messageKeywordEnabled true to enable + * @param errorMessageKeyword to use for custom messages in the schema * @return the builder */ - public Builder messageKeywordEnabled(boolean messageKeywordEnabled) { - this.customMessageEnabled = messageKeywordEnabled; + public Builder errorMessageKeyword(String errorMessageKeyword) { + this.errorMessageKeyword = errorMessageKeyword; return this; } /** @@ -956,7 +989,7 @@ public Builder writeOnly(Boolean writeOnly) { return this; } public SchemaValidatorsConfig build() { - return new ImmutableSchemaValidatorsConfig(applyDefaultsStrategy, cacheRefs, customMessageEnabled, + return new ImmutableSchemaValidatorsConfig(applyDefaultsStrategy, cacheRefs, errorMessageKeyword, executionContextCustomizer, failFast, formatAssertionsEnabled, handleNullableField, itemWalkListeners, javaSemantics, keywordWalkListeners, locale, losslessNarrowing, messageSource, openAPI3StyleDiscriminators, pathType, preloadJsonSchema, preloadJsonSchemaRefMaxNestingDepth, @@ -976,7 +1009,7 @@ public Builder strict(String keyword, boolean strict) { */ public static class ImmutableSchemaValidatorsConfig extends SchemaValidatorsConfig { public ImmutableSchemaValidatorsConfig(ApplyDefaultsStrategy applyDefaultsStrategy, boolean cacheRefs, - boolean customMessageSupported, ExecutionContextCustomizer executionContextCustomizer, boolean failFast, + String errorMessageKeyword, ExecutionContextCustomizer executionContextCustomizer, boolean failFast, Boolean formatAssertionsEnabled, boolean handleNullableField, List itemWalkListeners, boolean javaSemantics, Map> keywordWalkListenersMap, Locale locale, @@ -985,7 +1018,7 @@ public ImmutableSchemaValidatorsConfig(ApplyDefaultsStrategy applyDefaultsStrate List propertyWalkListeners, Boolean readOnly, RegularExpressionFactory regularExpressionFactory, JsonSchemaIdValidator schemaIdValidator, Map strictness, boolean typeLoose, Boolean writeOnly) { - super(applyDefaultsStrategy, cacheRefs, customMessageSupported, executionContextCustomizer, failFast, + super(applyDefaultsStrategy, cacheRefs, errorMessageKeyword, executionContextCustomizer, failFast, formatAssertionsEnabled, handleNullableField, itemWalkListeners, javaSemantics, keywordWalkListenersMap, locale, losslessNarrowing, messageSource, openAPI3StyleDiscriminators, pathType, preloadJsonSchema, preloadJsonSchemaRefMaxNestingDepth, propertyWalkListeners, readOnly, regularExpressionFactory, diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 1183f4ada..c0ca43d34 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -14,7 +14,7 @@ */ public abstract class ValidationMessageHandler { protected final ErrorMessageType errorMessageType; - protected final boolean customErrorMessagesEnabled; + protected final String errorMessageKeyword; protected final MessageSource messageSource; protected final Keyword keyword; protected final JsonSchema parentSchema; @@ -23,7 +23,7 @@ public abstract class ValidationMessageHandler { protected final JsonSchema evaluationParentSchema; protected final Map errorMessage; - protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean customErrorMessagesEnabled, + protected ValidationMessageHandler(ErrorMessageType errorMessageType, String errorMessageKeyword, MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation, JsonNodePath evaluationPath) { ErrorMessageType currentErrorMessageType = errorMessageType; @@ -32,14 +32,15 @@ protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean cu this.evaluationPath = Objects.requireNonNull(evaluationPath); this.parentSchema = parentSchema; this.evaluationParentSchema = null; - this.customErrorMessagesEnabled = customErrorMessagesEnabled; + this.errorMessageKeyword = errorMessageKeyword; this.keyword = keyword; Map currentErrorMessage = null; if (this.keyword != null) { - if (this.customErrorMessagesEnabled && keyword != null && parentSchema != null) { - currentErrorMessage = getErrorMessage(parentSchema.getSchemaNode(), keyword.getValue()); + if (this.errorMessageKeyword != null && keyword != null && parentSchema != null) { + currentErrorMessage = getErrorMessage(this.errorMessageKeyword, parentSchema.getSchemaNode(), + keyword.getValue()); } String errorCodeKey = getErrorCodeKey(keyword.getValue()); if (errorCodeKey != null && this.parentSchema != null) { @@ -60,7 +61,7 @@ protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean cu * Constructor to create a copy using fields. * * @param errorMessageType the error message type - * @param customErrorMessagesEnabled whether custom error msessages are enabled + * @param errorMessageKeyword the error message keyword * @param messageSource the message source * @param keyword the keyword * @param parentSchema the parent schema @@ -69,11 +70,11 @@ protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean cu * @param evaluationParentSchema the evaluation parent schema * @param errorMessage the error message */ - protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean customErrorMessagesEnabled, + protected ValidationMessageHandler(ErrorMessageType errorMessageType, String errorMessageKeyword, MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonSchema evaluationParentSchema, Map errorMessage) { this.errorMessageType = errorMessageType; - this.customErrorMessagesEnabled = customErrorMessagesEnabled; + this.errorMessageKeyword = errorMessageKeyword; this.messageSource = messageSource; this.keyword = keyword; this.parentSchema = parentSchema; @@ -104,9 +105,9 @@ protected ErrorMessageType getErrorMessageType() { * @param keyword the keyword * @return the custom error message */ - protected Map getErrorMessage(JsonNode schemaNode, String keyword) { + protected Map getErrorMessage(String errorMessageKeyword, JsonNode schemaNode, String keyword) { final JsonSchema parentSchema = this.parentSchema; - final JsonNode message = getMessageNode(schemaNode, parentSchema, keyword); + final JsonNode message = getMessageNode(errorMessageKeyword, schemaNode, parentSchema, keyword); if (message != null) { JsonNode messageNode = message.get(keyword); if (messageNode != null) { @@ -126,16 +127,18 @@ protected Map getErrorMessage(JsonNode schemaNode, String keywor return Collections.emptyMap(); } - protected JsonNode getMessageNode(JsonNode schemaNode, JsonSchema parentSchema, String pname) { - if (schemaNode.get("message") != null && schemaNode.get("message").get(pname) != null) { - return schemaNode.get("message"); + protected JsonNode getMessageNode(String errorMessageKeyword, JsonNode schemaNode, JsonSchema parentSchema, + String pname) { + if (schemaNode.get(errorMessageKeyword) != null && schemaNode.get(errorMessageKeyword).get(pname) != null) { + return schemaNode.get(errorMessageKeyword); } JsonNode messageNode; - messageNode = schemaNode.get("message"); + messageNode = schemaNode.get(errorMessageKeyword); if (messageNode == null && parentSchema != null) { - messageNode = parentSchema.schemaNode.get("message"); + messageNode = parentSchema.schemaNode.get(errorMessageKeyword); if (messageNode == null) { - return getMessageNode(parentSchema.schemaNode, parentSchema.getParentSchema(), pname); + return getMessageNode(errorMessageKeyword, parentSchema.schemaNode, parentSchema.getParentSchema(), + pname); } } return messageNode; From 6c8e5b26d9367d147d84ac841bca4efde4ff2d15 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:12:00 +0800 Subject: [PATCH 03/16] Add tests --- .../schema/SchemaValidatorsConfigTest.java | 48 ++++++++++++++ .../schema/ValidationMessageHandlerTest.java | 64 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/test/java/com/networknt/schema/ValidationMessageHandlerTest.java diff --git a/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java b/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java index 511eac987..1e013e6c3 100644 --- a/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java +++ b/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java @@ -25,6 +25,7 @@ /** * Test for SchemaValidatorsConfig. */ +@SuppressWarnings("deprecation") class SchemaValidatorsConfigTest { @Test void defaultEcma262Validator() { @@ -46,4 +47,51 @@ void setEcma262Validator() { assertFalse(config.isEcma262Validator()); } + @Test + void constructorPathType() { + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + assertEquals(PathType.LEGACY, config.getPathType()); + } + + @Test + void builderPathType() { + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build(); + assertEquals(PathType.JSON_POINTER, config.getPathType()); + } + + @Test + void constructorCustomMessageSupported() { + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + assertEquals(true, config.isCustomMessageSupported()); + } + + @Test + void builderCustomMessageSupported() { + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build(); + assertEquals(false, config.isCustomMessageSupported()); + } + + @Test + void constructorHandleNullableField() { + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + assertEquals(true, config.isHandleNullableField()); + } + + @Test + void builderHandleNullableField() { + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build(); + assertEquals(false, config.isHandleNullableField()); + } + + @Test + void constructorMutable() { + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + assertDoesNotThrow(() -> config.setFailFast(true)); + } + + @Test + void builderImmutable() { + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build(); + assertThrows(UnsupportedOperationException.class, () -> config.setFailFast(true)); + } } diff --git a/src/test/java/com/networknt/schema/ValidationMessageHandlerTest.java b/src/test/java/com/networknt/schema/ValidationMessageHandlerTest.java new file mode 100644 index 000000000..a0b9c3cec --- /dev/null +++ b/src/test/java/com/networknt/schema/ValidationMessageHandlerTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * ValidationMessageHandlerTest. + */ +class ValidationMessageHandlerTest { + @Test + void errorMessage() { + String schemaData = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"required\": [\r\n" + + " \"foo\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"foo\": {\r\n" + + " \"type\": \"integer\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"additionalProperties\": false,\r\n" + + " \"errorMessage\": {\r\n" + + " \"type\": \"should be an object\",\r\n" + + " \"required\": \"should have property foo\",\r\n" + + " \"additionalProperties\": \"should not have properties other than foo\"\r\n" + + " }\r\n" + + "}"; + String inputData = "{\r\n" + + " \"foo\": \"a\",\r\n" + + " \"bar\": 2\r\n" + + "}"; + JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, + SchemaValidatorsConfig.builder().errorMessageKeyword("errorMessage").build()); + List messages = schema.validate(inputData, InputFormat.JSON).stream().collect(Collectors.toList()); + assertFalse(messages.isEmpty()); + assertEquals("/foo", messages.get(0).getInstanceLocation().toString()); + assertEquals("should be an object", messages.get(0).getMessage()); + assertEquals("", messages.get(1).getInstanceLocation().toString()); + assertEquals("should not have properties other than foo", messages.get(1).getMessage()); + } + +} From 2d4bd8232f0cc0e2d283c83cdc31457c55dcacd9 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 16 Jun 2024 12:00:30 +0800 Subject: [PATCH 04/16] Refactor --- .../schema/SchemaValidatorsConfig.java | 87 +++++++++++++++---- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 495a42602..b84605e7d 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -34,6 +34,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.function.Consumer; /** * Configuration for validators. @@ -84,7 +85,7 @@ public class SchemaValidatorsConfig { * field is nullable && value is field: null --> it is up to the type validator * using the SchemaValidator to handle it. */ - private boolean handleNullableField = true; + private boolean nullableKeywordEnabled = true; private final WalkListenerRunner itemWalkListenerRunner; @@ -120,7 +121,7 @@ public class SchemaValidatorsConfig { * oneOf, anyOf and allOf as described on GitHub. */ - private boolean openAPI3StyleDiscriminators = false; + private boolean discriminatorKeywordEnabled = false; /** * The approach used to generate paths in reported messages, logs and errors. Default is the legacy "JSONPath-like" approach. @@ -203,10 +204,10 @@ public SchemaValidatorsConfig() { SchemaValidatorsConfig(ApplyDefaultsStrategy applyDefaultsStrategy, boolean cacheRefs, String errorMessageKeyword, ExecutionContextCustomizer executionContextCustomizer, boolean failFast, - Boolean formatAssertionsEnabled, boolean handleNullableField, + Boolean formatAssertionsEnabled, boolean nullableKeywordEnabled, List itemWalkListeners, boolean javaSemantics, Map> keywordWalkListenersMap, Locale locale, boolean losslessNarrowing, - MessageSource messageSource, boolean openAPI3StyleDiscriminators, PathType pathType, + MessageSource messageSource, boolean discriminatorKeywordEnabled, PathType pathType, boolean preloadJsonSchema, int preloadJsonSchemaRefMaxNestingDepth, List propertyWalkListeners, Boolean readOnly, RegularExpressionFactory regularExpressionFactory, JsonSchemaIdValidator schemaIdValidator, @@ -218,14 +219,14 @@ public SchemaValidatorsConfig() { this.executionContextCustomizer = executionContextCustomizer; this.failFast = failFast; this.formatAssertionsEnabled = formatAssertionsEnabled; - this.handleNullableField = handleNullableField; + this.nullableKeywordEnabled = nullableKeywordEnabled; this.itemWalkListeners = itemWalkListeners; this.javaSemantics = javaSemantics; this.keywordWalkListenersMap = keywordWalkListenersMap; this.locale = locale; this.losslessNarrowing = losslessNarrowing; this.messageSource = messageSource; - this.openAPI3StyleDiscriminators = openAPI3StyleDiscriminators; + this.discriminatorKeywordEnabled = discriminatorKeywordEnabled; this.pathType = pathType; this.preloadJsonSchema = preloadJsonSchema; this.preloadJsonSchemaRefMaxNestingDepth = preloadJsonSchemaRefMaxNestingDepth; @@ -440,8 +441,23 @@ public boolean isFailFast() { return this.failFast; } + /** + * Deprecated use {{@link #isNullableKeywordEnabled()} instead. + * + * @return true if the nullable keyword is enabled + */ + @Deprecated public boolean isHandleNullableField() { - return this.handleNullableField; + return isNullableKeywordEnabled(); + } + + /** + * Gets if the nullable keyword is enabled. + * + * @return true if the nullable keyword is enabled + */ + public boolean isNullableKeywordEnabled() { + return this.nullableKeywordEnabled; } public boolean isJavaSemantics() { @@ -454,12 +470,24 @@ public boolean isLosslessNarrowing() { /** * Indicates whether OpenAPI 3 style discriminators should be supported + *

+ * Deprecated use {{@link #isDiscriminatorKeywordEnabled()} instead. * * @return true in case discriminators are enabled * @since 1.0.51 */ + @Deprecated public boolean isOpenAPI3StyleDiscriminators() { - return this.openAPI3StyleDiscriminators; + return isDiscriminatorKeywordEnabled(); + } + + /** + * Gets if the discriminator keyword is enabled. + * + * @return true if the discriminator keyword is enabled + */ + public boolean isDiscriminatorKeywordEnabled() { + return this.discriminatorKeywordEnabled; } /** @@ -585,7 +613,7 @@ public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { } public void setHandleNullableField(boolean handleNullableField) { - this.handleNullableField = handleNullableField; + this.nullableKeywordEnabled = handleNullableField; } public void setJavaSemantics(boolean javaSemantics) { @@ -652,7 +680,7 @@ public void setMessageSource(MessageSource messageSource) { * @since 1.0.51 */ public void setOpenAPI3StyleDiscriminators(boolean openAPI3StyleDiscriminators) { - this.openAPI3StyleDiscriminators = openAPI3StyleDiscriminators; + this.discriminatorKeywordEnabled = openAPI3StyleDiscriminators; } /** @@ -743,14 +771,14 @@ public static class Builder { private ExecutionContextCustomizer executionContextCustomizer = null; private boolean failFast = false; private Boolean formatAssertionsEnabled = false; - private boolean handleNullableField = false; + private boolean nullableKeywordEnabled = false; private List itemWalkListeners = new ArrayList<>(); private boolean javaSemantics = false; private Map> keywordWalkListeners = new HashMap<>(); private Locale locale = null; // This must be null to use Locale.getDefault() as the default can be changed private boolean losslessNarrowing = false; private MessageSource messageSource = null; - private boolean openAPI3StyleDiscriminators = false; + private boolean discriminatorKeywordEnabled = false; private PathType pathType = PathType.JSON_POINTER; private boolean preloadJsonSchema = true; private int preloadJsonSchemaRefMaxNestingDepth = DEFAULT_PRELOAD_JSON_SCHEMA_REF_MAX_NESTING_DEPTH; @@ -847,7 +875,7 @@ public Builder formatAssertionsEnabled(Boolean formatAssertionsEnabled) { * @return the builder */ public Builder nullableKeywordEnabled(boolean nullableKeywordEnabled) { - this.handleNullableField = nullableKeywordEnabled; + this.nullableKeywordEnabled = nullableKeywordEnabled; return this; } public Builder itemWalkListeners(List itemWalkListeners) { @@ -900,7 +928,7 @@ public Builder messageSource(MessageSource messageSource) { * @return the builder */ public Builder discriminatorKeywordEnabled(boolean discriminatorKeywordEnabled) { - this.openAPI3StyleDiscriminators = discriminatorKeywordEnabled; + this.discriminatorKeywordEnabled = discriminatorKeywordEnabled; return this; } /** @@ -990,9 +1018,9 @@ public Builder writeOnly(Boolean writeOnly) { } public SchemaValidatorsConfig build() { return new ImmutableSchemaValidatorsConfig(applyDefaultsStrategy, cacheRefs, errorMessageKeyword, - executionContextCustomizer, failFast, formatAssertionsEnabled, handleNullableField, + executionContextCustomizer, failFast, formatAssertionsEnabled, nullableKeywordEnabled, itemWalkListeners, javaSemantics, keywordWalkListeners, locale, losslessNarrowing, messageSource, - openAPI3StyleDiscriminators, pathType, preloadJsonSchema, preloadJsonSchemaRefMaxNestingDepth, + discriminatorKeywordEnabled, pathType, preloadJsonSchema, preloadJsonSchemaRefMaxNestingDepth, propertyWalkListeners, readOnly, regularExpressionFactory, schemaIdValidator, strictness, typeLoose, writeOnly); } @@ -1000,6 +1028,33 @@ public Builder strict(String keyword, boolean strict) { this.strictness.put(Objects.requireNonNull(keyword, "keyword cannot be null"), strict); return this; } + public Builder keywordWalkListener(String keyword, JsonSchemaWalkListener keywordWalkListener) { + this.keywordWalkListeners.computeIfAbsent(keyword, key -> new ArrayList<>()).add(keywordWalkListener); + return this; + } + public Builder keywordWalkListener(JsonSchemaWalkListener keywordWalkListener) { + return keywordWalkListener(ALL_KEYWORD_WALK_LISTENER_KEY, keywordWalkListener); + } + public Builder keywordWalkListeners(Consumer>> keywordWalkListeners) { + keywordWalkListeners.accept(this.keywordWalkListeners); + return this; + } + public Builder propertyWalkListener(JsonSchemaWalkListener propertyWalkListener) { + this.propertyWalkListeners.add(propertyWalkListener); + return this; + } + public Builder propertyWalkListeners(Consumer> propertyWalkListeners) { + propertyWalkListeners.accept(this.propertyWalkListeners); + return this; + } + public Builder itemWalkListener(JsonSchemaWalkListener itemWalkListener) { + this.itemWalkListeners.add(itemWalkListener); + return this; + } + public Builder itemWalkListeners(Consumer> itemWalkListeners) { + itemWalkListeners.accept(this.itemWalkListeners); + return this; + } } /** From e626c90ee1c92f87824b610da8de9dfbaaa6bd2b Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:58:45 +0800 Subject: [PATCH 05/16] Refactor --- doc/config.md | 3 +-- doc/upgrading.md | 26 +++++++++++++++++++ .../schema/SchemaValidatorsConfig.java | 7 +++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/doc/config.md b/doc/config.md index c4ab32962..a130f3274 100644 --- a/doc/config.md +++ b/doc/config.md @@ -13,8 +13,7 @@ Most of the configuration flags are used to control the difference between Swagg When you create a `JsonSchema` instance from the `JsonSchemaFactory`, you can pass an object of SchemaValidatorsConfig as the second parameter. ```java -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.setTypeLoose(false); +SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().typeLoose(false).build(); JsonSchema jsonSchema = JsonSchemaFactory.getInstance().getSchema(schema, config); ``` diff --git a/doc/upgrading.md b/doc/upgrading.md index 506e96761..3be62a1f5 100644 --- a/doc/upgrading.md +++ b/doc/upgrading.md @@ -4,6 +4,32 @@ This library can contain breaking changes in `minor` version releases. This contains information on the notable or breaking changes in each version. +### 1.4.1 + +The `SchemaValidatorsConfig` constructor has been deprecated. Use the `SchemaValidators.builder` to create an instance instead. + +Note that there are differences in defaults from the builder vs the constructor. + +The following builder creates the same values as the constructor previously. + +```java +SchemaValidatorsConfig config = SchemaValidatorsConfig.builder() + .pathType(PathType.LEGACY) + .errorMessageKeyword("message") + .nullableKeywordEnabled(true) + .build(); +``` + +The following configurations were renamed with the old ones deprecated +* `handleNullableField` -> `nullableKeywordEnabled` +* `openAPI3StyleDiscriminators` -> `discriminatorKeywordEnabled` +* `customMessageSupported` -> `errorMessageKeyword` + +The following defaults were changed in the builder vs the constructor +* `pathType` from `PathType.LEGACY` to `PathType.JSON_POINTER` +* `handleNullableField` from `true` to `false` +* `customMessageSupported` from `true` to `false` + ### 1.4.0 This contains breaking changes diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index b84605e7d..ae3a4e578 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -183,6 +183,13 @@ public class SchemaValidatorsConfig { * This is deprecated in favor of using the builder * {@link SchemaValidatorsConfig#builder()} to create an instance. Migration * note: The builder has different defaults from the constructor. + *

+     * SchemaValidatorsConfig config = SchemaValidatorsConfig.builder()
+     *     .pathType(PathType.LEGACY)
+     *     .errorMessageKeyword("message")
+     *     .nullableKeywordEnabled(true)
+     *     .build();
+     * 
*