Skip to content

Commit

Permalink
Improve vocabulary support (#953)
Browse files Browse the repository at this point in the history
  • Loading branch information
justin-tay committed Feb 6, 2024
1 parent 51fd82b commit a61cf42
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 59 deletions.
25 changes: 21 additions & 4 deletions src/main/java/com/networknt/schema/JsonMetaSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,27 @@ public JsonMetaSchema build() {
if (this.specification != null) {
if (this.specification.getVersionFlagValue() >= SpecVersion.VersionFlag.V201909.getVersionFlagValue()) {
if (!this.uri.equals(this.specification.getId())) {
String validation = Vocabularies.getVocabulary(specification, "validation");
if (!this.vocabularies.getOrDefault(validation, false)) {
for (String keywordToRemove : Vocabularies.getKeywords("validation")) {
kwords.remove(keywordToRemove);
// The current design is such that the keyword map can contain things that aren't actually keywords
// This means need to remove what can't be found instead of creating from scratch
Map<String, Boolean> vocabularies = JsonSchemaFactory.checkVersion(this.specification)
.getInstance().getVocabularies();
Set<String> current = this.vocabularies.keySet();
Map<String, String> format = new HashMap<>();
format.put(Vocabulary.V202012_FORMAT_ANNOTATION.getId(), Vocabulary.V202012_FORMAT_ASSERTION.getId());
format.put(Vocabulary.V202012_FORMAT_ASSERTION.getId(), Vocabulary.V202012_FORMAT_ANNOTATION.getId());
for (String vocabularyId : vocabularies.keySet()) {
if (!current.contains(vocabularyId)) {
String formatVocab = format.get(vocabularyId);
if (formatVocab != null) {
if (current.contains(formatVocab)) {
// Skip as the assertion and annotation keywords are the same
continue;
}
}
Vocabulary vocabulary = Vocabularies.getVocabulary(vocabularyId);
for (String keywordToRemove : vocabulary.getKeywords()) {
kwords.remove(keywordToRemove);
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/networknt/schema/JsonSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
import java.util.function.Consumer;

/**
* Used for creating a schema with validators for validating inputs. This is
* created using {@link JsonSchemaFactory#getInstance(VersionFlag, Consumer)}
* and should be cached for performance.
* <p>
* This is the core of json constraint implementation. It parses json constraint
* file and generates JsonValidators. The class is thread safe, once it is
* constructed, it can be used to validate multiple json data concurrently.
Expand Down
19 changes: 14 additions & 5 deletions src/main/java/com/networknt/schema/JsonSchemaFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.SpecVersion.VersionFlag;
import com.networknt.schema.resource.*;
import com.networknt.schema.resource.DefaultSchemaLoader;
import com.networknt.schema.resource.SchemaLoader;
import com.networknt.schema.resource.SchemaLoaders;
import com.networknt.schema.resource.SchemaMapper;
import com.networknt.schema.resource.SchemaMappers;
import com.networknt.schema.serialization.JsonMapperFactory;
import com.networknt.schema.serialization.YamlMapperFactory;

Expand All @@ -30,14 +34,20 @@
import java.io.InputStream;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;

/**
* Factory for building {@link JsonSchema} instances.
* <p>
* The factory should be typically be created using
* {@link #getInstance(VersionFlag, Consumer)}.
*/
public class JsonSchemaFactory {
private static final Logger logger = LoggerFactory
.getLogger(JsonSchemaFactory.class);
Expand Down Expand Up @@ -185,7 +195,7 @@ public static JsonSchemaFactory getInstance(SpecVersion.VersionFlag versionFlag)
* be used if the input does not specify a $schema.
*
* @param versionFlag the default dialect
* @param customizer to customze the factory
* @param customizer to customize the factory
* @return the factory
*/
public static JsonSchemaFactory getInstance(SpecVersion.VersionFlag versionFlag,
Expand Down Expand Up @@ -364,12 +374,11 @@ protected JsonMetaSchema loadMetaSchema(String id, SchemaValidatorsConfig config
// Process vocabularies
JsonNode vocabulary = schema.getSchemaNode().get("$vocabulary");
if (vocabulary != null) {
builder.vocabularies(new HashMap<>());
builder.vocabularies(new LinkedHashMap<>());
for(Entry<String, JsonNode> vocabs : vocabulary.properties()) {
builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue());
}
}

}
}
return builder.build();
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/com/networknt/schema/UnionTypeValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidat
private final List<JsonValidator> schemas = new ArrayList<JsonValidator>();
private final String error;


public UnionTypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNION_TYPE, validationContext);
StringBuilder errorBuilder = new StringBuilder();
Expand Down Expand Up @@ -105,4 +104,9 @@ public void preloadJsonSchema() {
validator.preloadJsonSchema();
}
}

@Override
public String getKeyword() {
return "type";
}
}
3 changes: 1 addition & 2 deletions src/main/java/com/networknt/schema/Version202012.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ public JsonMetaSchema getInstance() {
new NonValidationKeyword("contentSchema"),
new NonValidationKeyword("examples"),
new NonValidationKeyword("then"),
new NonValidationKeyword("else"),
new NonValidationKeyword("additionalItems")
new NonValidationKeyword("else")
))
.vocabularies(VOCABULARY)
.build();
Expand Down
68 changes: 21 additions & 47 deletions src/main/java/com/networknt/schema/Vocabularies.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,69 +15,43 @@
*/
package com.networknt.schema;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Vocabularies.
*/
public class Vocabularies {
private static final Map<String, List<String>> KEYWORDS_MAPPING;
private static final Map<String, Vocabulary> VALUES;

static {
Map<String, List<String>> mapping = new HashMap<>();
List<String> validation = new ArrayList<>();
validation.add("type");
validation.add("enum");
validation.add("const");
Map<String, Vocabulary> mapping = new HashMap<>();
mapping.put(Vocabulary.V201909_CORE.getId(), Vocabulary.V201909_CORE);
mapping.put(Vocabulary.V201909_APPLICATOR.getId(), Vocabulary.V201909_APPLICATOR);
mapping.put(Vocabulary.V201909_VALIDATION.getId(), Vocabulary.V201909_VALIDATION);
mapping.put(Vocabulary.V201909_META_DATA.getId(), Vocabulary.V201909_META_DATA);
mapping.put(Vocabulary.V201909_FORMAT.getId(), Vocabulary.V201909_FORMAT);
mapping.put(Vocabulary.V201909_CONTENT.getId(), Vocabulary.V201909_CONTENT);

validation.add("multipleOf");
validation.add("maximum");
validation.add("exclusiveMaximum");
validation.add("minimum");
validation.add("exclusiveMinimum");

validation.add("maxLength");
validation.add("minLength");
validation.add("pattern");
mapping.put(Vocabulary.V202012_CORE.getId(), Vocabulary.V202012_CORE);
mapping.put(Vocabulary.V202012_APPLICATOR.getId(), Vocabulary.V202012_APPLICATOR);
mapping.put(Vocabulary.V202012_UNEVALUATED.getId(), Vocabulary.V202012_UNEVALUATED);
mapping.put(Vocabulary.V202012_VALIDATION.getId(), Vocabulary.V202012_VALIDATION);
mapping.put(Vocabulary.V202012_META_DATA.getId(), Vocabulary.V202012_META_DATA);
mapping.put(Vocabulary.V202012_FORMAT_ANNOTATION.getId(), Vocabulary.V202012_FORMAT_ANNOTATION);
mapping.put(Vocabulary.V202012_FORMAT_ASSERTION.getId(), Vocabulary.V202012_FORMAT_ASSERTION);
mapping.put(Vocabulary.V202012_CONTENT.getId(), Vocabulary.V202012_CONTENT);

validation.add("maxItems");
validation.add("minItems");
validation.add("uniqueItems");
validation.add("maxContains");
validation.add("minContains");

validation.add("maxProperties");
validation.add("minProperties");
validation.add("required");
validation.add("dependentRequired");

mapping.put("validation", validation);

KEYWORDS_MAPPING = mapping;
VALUES = mapping;
}

/**
* Gets the keywords associated with a vocabulary.
* Gets the vocabulary given its id.
*
* @param vocabulary the vocabulary
* @return the keywords
* @return the vocabulary
*/
public static List<String> getKeywords(String vocabulary) {
return KEYWORDS_MAPPING.get(vocabulary);
}

/**
* Gets the vocabulary IRI.
*
* @param specification the specification
* @param vocabulary the vocabulary
* @return the vocabulary IRI
*/
public static String getVocabulary(SpecVersion.VersionFlag specification, String vocabulary) {
String base = specification.getId().substring(0, specification.getId().lastIndexOf('/'));
return base + "/vocab/" + vocabulary;
public static Vocabulary getVocabulary(String vocabulary) {
return VALUES.get(vocabulary);
}
}
133 changes: 133 additions & 0 deletions src/main/java/com/networknt/schema/Vocabulary.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

/**
* Represents a vocabulary in meta schema.
* <p>
* This contains the id and the related keywords.
*/
public class Vocabulary {

// 2019-09
public static final Vocabulary V201909_CORE = new Vocabulary("https://json-schema.org/draft/2019-09/vocab/core",
"$id", "$schema", "$anchor", "$ref", "$recursiveRef", "$recursiveAnchor", "$vocabulary", "$comment",
"$defs");
public static final Vocabulary V201909_APPLICATOR = new Vocabulary(
"https://json-schema.org/draft/2019-09/vocab/applicator", "additionalItems", "unevaluatedItems", "items",
"contains", "additionalProperties", "unevaluatedProperties", "properties", "patternProperties",
"dependentSchemas", "propertyNames", "if", "then", "else", "allOf", "anyOf", "oneOf", "not");
public static final Vocabulary V201909_VALIDATION = new Vocabulary(
"https://json-schema.org/draft/2019-09/vocab/validation", "multipleOf", "maximum", "exclusiveMaximum",
"minimum", "exclusiveMinimum", "maxLength", "minLength", "pattern", "maxItems", "minItems", "uniqueItems",
"maxContains", "minContains", "maxProperties", "minProperties", "required", "dependentRequired", "const",
"enum", "type");
public static final Vocabulary V201909_META_DATA = new Vocabulary(
"https://json-schema.org/draft/2019-09/vocab/meta-data", "title", "description", "default", "deprecated",
"readOnly", "writeOnly", "examples");
public static final Vocabulary V201909_FORMAT = new Vocabulary("https://json-schema.org/draft/2019-09/vocab/format",
"format");
public static final Vocabulary V201909_CONTENT = new Vocabulary(
"https://json-schema.org/draft/2019-09/vocab/content", "contentMediaType", "contentEncoding",
"contentSchema");

// 2020-12
public static final Vocabulary V202012_CORE = new Vocabulary("https://json-schema.org/draft/2020-12/vocab/core",
"$id", "$schema", "$ref", "$anchor", "$dynamicRef", "$dynamicAnchor", "$vocabulary", "$comment", "$defs");
public static final Vocabulary V202012_APPLICATOR = new Vocabulary(
"https://json-schema.org/draft/2020-12/vocab/applicator", "prefixItems", "items", "contains",
"additionalProperties", "properties", "patternProperties", "dependentSchemas", "propertyNames", "if",
"then", "else", "allOf", "anyOf", "oneOf", "not");
public static final Vocabulary V202012_UNEVALUATED = new Vocabulary(
"https://json-schema.org/draft/2020-12/vocab/unevaluated", "unevaluatedItems", "unevaluatedProperties");
public static final Vocabulary V202012_VALIDATION = new Vocabulary(
"https://json-schema.org/draft/2020-12/vocab/validation", "type", "const", "enum", "multipleOf", "maximum",
"exclusiveMaximum", "minimum", "exclusiveMinimum", "maxLength", "minLength", "pattern", "maxItems",
"minItems", "uniqueItems", "maxContains", "minContains", "maxProperties", "minProperties", "required",
"dependentRequired");
public static final Vocabulary V202012_META_DATA = new Vocabulary(
"https://json-schema.org/draft/2020-12/vocab/meta-data", "title", "description", "default", "deprecated",
"readOnly", "writeOnly", "examples");
public static final Vocabulary V202012_FORMAT_ANNOTATION = new Vocabulary(
"https://json-schema.org/draft/2020-12/vocab/format-annotation", "format");
public static final Vocabulary V202012_FORMAT_ASSERTION = new Vocabulary(
"https://json-schema.org/draft/2020-12/vocab/format-assertion", "format");
public static final Vocabulary V202012_CONTENT = new Vocabulary(
"https://json-schema.org/draft/2020-12/vocab/content", "contentEncoding", "contentMediaType",
"contentSchema");

private final String id;
private final Set<String> keywords;

/**
* Constructor.
*
* @param id the id
* @param keywords the keywords
*/
public Vocabulary(String id, String... keywords) {
this.id = id;
this.keywords = new LinkedHashSet<>();
for (String keyword : keywords) {
this.keywords.add(keyword);
}
}

/**
* The id of the vocabulary.
*
* @return the id
*/
public String getId() {
return id;
}

/**
* The keywords in the vocabulary.
*
* @return the keywords
*/
public Set<String> getKeywords() {
return keywords;
}

@Override
public int hashCode() {
return Objects.hash(id, keywords);
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Vocabulary other = (Vocabulary) obj;
return Objects.equals(id, other.id) && Objects.equals(keywords, other.keywords);
}

@Override
public String toString() {
return "Vocabulary [id=" + id + ", keywords=" + keywords + "]";
}

}
Loading

0 comments on commit a61cf42

Please sign in to comment.