xref: /aosp_15_r20/external/json-schema-validator/doc/custom-meta-schema.md (revision 78c4dd6aa35290980cdcd1623a7e337e8d021c7c)
1# Customizing Meta-Schemas, Vocabularies, Keywords and Formats
2
3The meta-schemas, vocabularies, keywords and formats can be customized with appropriate configuration of the `JsonSchemaFactory` that is used to create instances of `JsonSchema`.
4
5## Creating a custom keyword
6
7A custom keyword can be implemented by implementing the `com.networknt.schema.Keyword` interface.
8
9```java
10public class EqualsKeyword implements Keyword {
11    @Override
12    public String getValue() {
13        return "equals";
14    }
15    @Override
16    public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
17            JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)
18            throws JsonSchemaException, Exception {
19        return new EqualsValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, this, validationContext, false);
20    }
21}
22```
23
24```java
25public class EqualsValidator extends BaseJsonValidator {
26    private static ErrorMessageType ERROR_MESSAGE_TYPE = new ErrorMessageType() {
27        @Override
28        public String getErrorCode() {
29            return "equals";
30        }
31    };
32
33    private final String value;
34    public EqualsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
35            JsonSchema parentSchema, Keyword keyword,
36            ValidationContext validationContext, boolean suppressSubSchemaRetrieval) {
37        super(schemaLocation, evaluationPath, schemaNode, parentSchema, ERROR_MESSAGE_TYPE, keyword, validationContext,
38                suppressSubSchemaRetrieval);
39        this.value = schemaNode.textValue();
40    }
41    @Override
42    public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
43            JsonNodePath instanceLocation) {
44        if (!node.asText().equals(value)) {
45            return Collections
46                    .singleton(message().message("{0}: must be equal to ''{1}''")
47                            .arguments(value)
48                            .instanceLocation(instanceLocation).instanceNode(node).build());
49        };
50        return Collections.emptySet();
51    }
52}
53```
54
55## Adding a keyword to a standard dialect
56
57A custom keyword can be added to a standard dialect by customizing its meta-schema which is identified by its IRI.
58
59The following adds a custom keyword to the Draft 2020-12 dialect.
60
61```java
62JsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
63        .keyword(new EqualsKeyword())
64        .build();
65JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
66```
67
68## Creating a custom meta-schema
69
70A custom meta-schema can be created by using a standard dialect as a base.
71
72The following creates a custom meta-schema `https://www.example.com/schema` with a custom keyword using the Draft 2020-12 dialect as a base.
73
74```java
75JsonMetaSchema dialect = JsonMetaSchema.getV202012();
76JsonMetaSchema metaSchema = JsonMetaSchema.builder("https://www.example.com/schema", dialect)
77        .keyword(new EqualsKeyword())
78        .build();
79JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
80```
81
82## Associating vocabularies to a dialect
83
84Custom vocabularies can be associated with a particular dialect by configuring a `com.networknt.schema.VocabularyFactory` on its meta-schema.
85
86```java
87VocabularyFactory vocabularyFactory = iri -> {
88    if ("https://www.example.com/vocab/equals".equals(iri)) {
89        return new Vocabulary("https://www.example.com/vocab/equals", new EqualsKeyword());
90    }
91    return null;
92};
93JsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
94        .vocabularyFactory(vocabularyFactory)
95        .build();
96JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
97```
98
99The following custom meta-schema `https://www.example.com/schema` will use the custom vocabulary `https://www.example.com/vocab/equals`.
100
101```json
102{
103  "$schema": "https://json-schema.org/draft/2020-12/schema",
104  "$id": "https://www.example.com/schema",
105  "$vocabulary": {
106    "https://www.example.com/vocab/equals": true,
107    "https://json-schema.org/draft/2020-12/vocab/applicator": true,
108    "https://json-schema.org/draft/2020-12/vocab/core": true
109  },
110  "allOf": [
111    { "$ref": "https://json-schema.org/draft/2020-12/meta/applicator" },
112    { "$ref": "https://json-schema.org/draft/2020-12/meta/core" }
113  ]
114}
115```
116
117Note that `"https://www.example.com/vocab/equals": true` means that if the vocabulary is unknown the meta-schema will fail to successfully load while `"https://www.example.com/vocab/equals": false` means that an unknown vocabulary will still successfully load.
118
119## Unknown keywords
120
121By default unknown keywords are treated as annotations. This can be customized by configuring a `com.networknt.schema.KeywordFactory` on its meta-schema.
122
123The following configuration will cause a `InvalidSchemaException` to be thrown if an unknown keyword is used.
124
125```java
126JsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
127        .unknownKeywordFactory(DisallowUnknownKeywordFactory.getInstance())
128        .build();
129JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
130```
131
132## Creating a custom format
133
134A custom format can be implemented by implementing the `com.networknt.schema.Format` interface.
135
136```java
137public class MatchNumberFormat implements Format {
138    private final BigDecimal compare;
139
140    public MatchNumberFormat(BigDecimal compare) {
141        this.compare = compare;
142    }
143    @Override
144    public boolean matches(ExecutionContext executionContext, ValidationContext validationContext, JsonNode value) {
145        JsonType nodeType = TypeFactory.getValueNodeType(value, validationContext.getConfig());
146        if (nodeType != JsonType.NUMBER && nodeType != JsonType.INTEGER) {
147            return true;
148        }
149        BigDecimal number = value.isBigDecimal() ? value.decimalValue() : BigDecimal.valueOf(value.doubleValue());
150        number = new BigDecimal(number.toPlainString());
151        return number.compareTo(compare) == 0;
152    }
153    @Override
154    public String getName() {
155        return "matchnumber";
156    }
157}
158```
159
160## Adding a format to a standard dialect
161
162A custom format can be added to a standard dialect by customizing its meta-schema which is identified by its IRI.
163
164The following adds a custom format to the Draft 2020-12 dialect.
165
166```java
167JsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
168        .format(new MatchNumberFormat(new BigDecimal("12345")))
169        .build();
170JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
171```
172
173## Customizing the format keyword
174
175The format keyword implementation to use can be customized by supplying a `FormatKeywordFactory` to the meta-schema that creates an instance of the subclass of `FormatKeyword`.
176
177```java
178JsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
179        .formatKeywordFactory(CustomFormatKeyword::new)
180        .build();
181JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
182```
183
184## Unknown formats
185
186By default unknown formats are ignored unless the format assertion vocabulary is used for that meta-schema. Note that the format annotation vocabulary with the configuration to enable format assertions is not equivalent to the format assertion vocabulary.
187
188To ensure that errors are raised when unknown formats are used, the `SchemaValidatorsConfig` can be configured to set `format` as strict.
189
190
191## Loading meta-schemas
192
193By default meta-schemas that aren't explicitly configured in the `JsonSchemaFactory` will be automatically loaded.
194
195This means that the following `JsonSchemaFactory` will still be able to process `$schema` with other dialects such as Draft 7 or Draft 2019-09 as the meta-schemas for those dialects will be automatically loaded. This will also attempt to load custom meta-schemas with custom vocabularies. Draft 2020-12 will be used by default if `$schema` is not defined.
196
197```java
198JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
199```
200
201If this is undesirable, for instance to restrict the meta-schemas used only to those explicitly configured in the `JsonSchemaFactory` a `com.networknt.schema.JsonMetaSchemaFactory` can be configured.
202
203```java
204JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012,
205        builder -> builder.metaSchemaFactory(DisallowUnknownJsonMetaSchemaFactory.getInstance()));
206```
207