Skip to content

Commit 70fd1aa

Browse files
feat: introduce sub-type resolver config option
1 parent d662742 commit 70fd1aa

File tree

11 files changed

+176
-34
lines changed

11 files changed

+176
-34
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Enable declaration of subtypes through `withSubtypeResolver(SubtypeResolver)` on `forTypesInGeneral()` (#24)
10+
11+
### Changed
12+
- Move custom definitions and type attribute overrides into `forTypesInGeneral()` (while preserving delegate setters on config builder)
813

914
## [4.3.0] - 2020-02-28
1015
### Changed

README.md

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -151,29 +151,30 @@ configBuilder.forTypesInGeneral()
151151
| 1 | `$schema` | Fixed to "http://json-schema.org/draft-07/schema#" – can be toggled on/off via `Option.SCHEMA_VERSION_INDICATOR`. |
152152
| 2 | `definitions` | Filled with sub-schemas to support circular references – via `Option.DEFINITIONS_FOR_ALL_OBJECTS` it can be configured whether only sub-schemas appearing more than once are included or all. |
153153
| 3 | `$ref` | Used with relative references to sub-schemas in `definitions`. |
154-
| 4 | `type` | Differentiating between `boolean`/`string`/`integer`/`number` for primitive/known types. `null` is added if a property is deemed nullable according to configuration (`SchemaGeneratorConfigPart.withNullableCheck()`). Arrays and sub-types of `Collection<?>` are treated as `array`, everything else as `object`. A declared type may be interpreted as another type according to configuration (`SchemaGeneratorConfigPart.withTargetTypeOverrideResolver()`). |
154+
| 4 | `type` | Differentiating between `boolean`/`string`/`integer`/`number` for primitive/known types. `null` is added if a property is deemed nullable according to configuration (`SchemaGeneratorConfigPart.withNullableCheck()`). Arrays and subtypes of `Collection<?>` are treated as `array`, everything else as `object`. A declared type may be interpreted as another type according to configuration (`SchemaGeneratorConfigPart.withTargetTypeOverrideResolver()`). |
155155
| 5 | `properties` | Listing all detected fields and/or methods in an `object`. Which ones are being included can be steered by various `Option`s or via one of the provided `OptionPreset`s as well as by ignoring individual ones via configuration (`SchemaGeneratorConfigPart.withIgnoreCheck()`). Names can be altered via configuration (`SchemaGeneratorConfigPart.withPropertyNameOverrideResolver()`). |
156156
| 6 | `items` | Indicating the type of `array`/`Collection` elements. |
157157
| 7 | `required` | Listing the names of fields/methods that are deemed mandatory according to configuration (`SchemaGeneratorConfigPart.withRequiredCheck()`). |
158158
| 8 | `allOf` | Used to combine general attributes derived from the type itself with attributes collected in the respective context of the associated field/method. |
159-
| 9 | `oneOf` | Used to indicate when a particular field/method can be of `type` `null`. |
160-
| 10 | `title` | Collected value according to configuration (`SchemaGeneratorConfigPart.withTitleResolver()`). |
161-
| 11 | `description` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDescriptionResolver()`). |
162-
| 12 | `const` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if only a single value was found. |
163-
| 13 | `enum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if multiple values were found. |
164-
| 14 | `default` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDefaultResolver()`). |
165-
| 15 | `additionalProperties` | Collected value according to configuration (`SchemaGeneratorConfigPart.withAdditionalPropertiesResolver()`). |
166-
| 16 | `patternProperties` | Collected value(s) according to configuration (`SchemaGeneratorConfigPart.withPatternPropertiesResolver()`). |
167-
| 17 | `minLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMinLengthResolver()`). |
168-
| 18 | `maxLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMaxLengthResolver()`). |
169-
| 19 | `format` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringFormatResolver()`). |
170-
| 20 | `pattern` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringPatternResolver()`). |
171-
| 21 | `minimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMinimumResolver()`). |
172-
| 22 | `exclusiveMinimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMinimumResolver()`). |
173-
| 23 | `maximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMaximumResolver()`). |
174-
| 24 | `exclusiveMaximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMaximumResolver()`). |
175-
| 25 | `multipleOf` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberMultipleOfResolver()`). |
176-
| 26 | `minItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMinItemsResolver()`). |
177-
| 27 | `maxItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMaxItemsResolver()`). |
178-
| 28 | `uniqueItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayUniqueItemsResolver()`). |
179-
| 29 | any other | You can directly manipulate the generated `ObjectNode` of a sub-schema – e.g. setting additional attributes – via configuration based on a given type in general (`SchemaGeneratorConfigBuilder.with(TypeAttributeOverride)`) and/or in the context of a particular field/method (`SchemaGeneratorConfigPart.withInstanceAttributeOverride()`). |
159+
| 9 | `anyOf` | Used to list alternatives according to configuration (`SchemaGeneratorGeneralConfigPart.withSubtypeResolver()`). |
160+
| 10 | `oneOf` | Used to indicate when a particular field/method can be of `type` `null`. |
161+
| 11 | `title` | Collected value according to configuration (`SchemaGeneratorConfigPart.withTitleResolver()`). |
162+
| 12 | `description` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDescriptionResolver()`). |
163+
| 13 | `const` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if only a single value was found. |
164+
| 14 | `enum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if multiple values were found. |
165+
| 15 | `default` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDefaultResolver()`). |
166+
| 16 | `additionalProperties` | Collected value according to configuration (`SchemaGeneratorConfigPart.withAdditionalPropertiesResolver()`). |
167+
| 17 | `patternProperties` | Collected value(s) according to configuration (`SchemaGeneratorConfigPart.withPatternPropertiesResolver()`). |
168+
| 18 | `minLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMinLengthResolver()`). |
169+
| 19 | `maxLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMaxLengthResolver()`). |
170+
| 20 | `format` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringFormatResolver()`). |
171+
| 21 | `pattern` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringPatternResolver()`). |
172+
| 22 | `minimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMinimumResolver()`). |
173+
| 23 | `exclusiveMinimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMinimumResolver()`). |
174+
| 24 | `maximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMaximumResolver()`). |
175+
| 25 | `exclusiveMaximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMaximumResolver()`). |
176+
| 26 | `multipleOf` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberMultipleOfResolver()`). |
177+
| 27 | `minItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMinItemsResolver()`). |
178+
| 28 | `maxItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMaxItemsResolver()`). |
179+
| 29 | `uniqueItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayUniqueItemsResolver()`). |
180+
| 30 | any other | You can directly manipulate the generated `ObjectNode` of a sub-schema – e.g. setting additional attributes – via configuration based on a given type in general (`SchemaGeneratorConfigBuilder.with(TypeAttributeOverride)`) and/or in the context of a particular field/method (`SchemaGeneratorConfigPart.withInstanceAttributeOverride()`). |

src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ public interface SchemaGeneratorConfig {
9595
CustomDefinition getCustomDefinition(ResolvedType javaType, SchemaGenerationContext context,
9696
CustomDefinitionProviderV2 ignoredDefinitionProvider);
9797

98+
/**
99+
* Look-up a declared type's subtypes in order to list those specifically (in an "{@value SchemaConstants#TAG_ANYOF}").
100+
*
101+
* @param javaType declared type to look-up subtypes for
102+
* @param context generation context (including a reference to the {@code TypeContext} for deriving a {@link ResolvedType} from a {@link Class})
103+
* @return subtypes to list as possible alternatives for the declared type (may be empty)
104+
*/
105+
List<ResolvedType> resolveSubtypes(ResolvedType javaType, SchemaGenerationContext context);
106+
98107
/**
99108
* Getter for the applicable type attribute overrides.
100109
*

src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public SchemaGeneratorConfig build() {
9595
*
9696
* @return configuration part responsible for handling types regardless of their declaration context
9797
*/
98-
public SchemaGeneratorTypeConfigPart<TypeScope> forTypesInGeneral() {
98+
public SchemaGeneratorGeneralConfigPart forTypesInGeneral() {
9999
return this.typesInGeneralConfigPart;
100100
}
101101

src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,13 @@ public Boolean isNullable(M member) {
141141

142142
/**
143143
* Setter for target type resolver, expecting the respective member and the default type as inputs.
144+
* <br>
145+
* For generally replacing one type with one or multiple of its subtypes, you may want to consider adding a {@link SubtypeResolver} via
146+
* {@link SchemaGeneratorConfigBuilder#forTypesInGeneral() forTypesInGeneral()} instead.
144147
*
145148
* @param resolver how to determine the alternative target type
146149
* @return this config part (for chaining)
150+
* @see SchemaGeneratorGeneralConfigPart#withSubtypeResolver(SubtypeResolver)
147151
*/
148152
public SchemaGeneratorConfigPart<M> withTargetTypeOverrideResolver(ConfigFunction<M, ResolvedType> resolver) {
149153
this.targetTypeOverrideResolvers.add(resolver);

src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorGeneralConfigPart.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
public class SchemaGeneratorGeneralConfigPart extends SchemaGeneratorTypeConfigPart<TypeScope> {
3131

3232
private final List<CustomDefinitionProviderV2> customDefinitionProviders = new ArrayList<>();
33+
private final List<SubtypeResolver> subtypeResolvers = new ArrayList<>();
3334
private final List<TypeAttributeOverride> typeAttributeOverrides = new ArrayList<>();
3435

3536
/**
@@ -54,6 +55,28 @@ public List<CustomDefinitionProviderV2> getCustomDefinitionProviders() {
5455
return Collections.unmodifiableList(this.customDefinitionProviders);
5556
}
5657

58+
/**
59+
* Adding a subtype resolver – if it returns null for a given type, the next subtype resolver will be applied.
60+
* <br>
61+
* If all subtype resolvers return null, there is none or a resolver returns an empty list, then the standard behaviour applies.
62+
*
63+
* @param subtypeResolver resolver for looking up a declared type's subtypes in order to list those specifically
64+
* @return this builder instance (for chaining)
65+
*/
66+
public SchemaGeneratorGeneralConfigPart withSubtypeResolver(SubtypeResolver subtypeResolver) {
67+
this.subtypeResolvers.add(subtypeResolver);
68+
return this;
69+
}
70+
71+
/**
72+
* Getter for the applicable subtype resolvers.
73+
*
74+
* @return registered subtype resolvers
75+
*/
76+
public List<SubtypeResolver> getSubtypeResolvers() {
77+
return Collections.unmodifiableList(this.subtypeResolvers);
78+
}
79+
5780
/**
5881
* Adding an override for type attributes – all of the registered overrides will be applied in the order of having been added.
5982
*
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2020 VicTools.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.github.victools.jsonschema.generator;
18+
19+
import com.fasterxml.classmate.ResolvedType;
20+
import java.util.List;
21+
22+
/**
23+
* Resolver for looking up a declared type's subtypes in order to list those specifically (in an "{@value SchemaConstants#TAG_ANYOF}").
24+
* <br>
25+
* Assumption being that "{@value SchemaConstants#TAG_ONEOF}" would require a schema validator to unnecessarily check against all listed sub-schemas
26+
* to ensure that only a single one is matching a given JSON instance. By making the sub-schemas mutually exclusive, the same semantics can be
27+
* achieved, but allowing the schema validator to ignore any sub-schemas after the first match was found.
28+
*/
29+
@FunctionalInterface
30+
public interface SubtypeResolver {
31+
32+
/**
33+
* Look-up the subtypes for a given type, that should be listed independently.
34+
* <br>
35+
* If it returns null, the next subtype resolver is expected to be applied.An empty list will result only in the originally declared type to be
36+
* considered.
37+
* <br>
38+
* Returning a list with a single entry will treat the declared type as one-to-one alias for the returned type. Alternatively, you may want to
39+
* only replace it in the context of a particular field/method through target type overrides.
40+
*
41+
* @param declaredType declared type (i.e. without type parameter information)
42+
* @param context generation context (including a reference to the {@code TypeContext} for deriving a {@link ResolvedType} from a {@link Class})
43+
* @return list of subtypes to represent as separate schemas; returning
44+
* @see SchemaGeneratorConfigPart#withTargetTypeOverrideResolver(ConfigFunction)
45+
*/
46+
List<ResolvedType> findSubtypes(ResolvedType declaredType, SchemaGenerationContext context);
47+
}

src/main/java/com/github/victools/jsonschema/generator/TypeContext.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ public final ResolvedType resolve(Type type, Type... typeParameters) {
6060
return this.typeResolver.resolve(type, typeParameters);
6161
}
6262

63+
/**
64+
* Resolve subtype considering the given super-types (potentially) known type parameters.
65+
*
66+
* @param supertype already resolved super type
67+
* @param subtype erased java subtype to resolve
68+
* @return resolved subtype
69+
* @see TypeResolver#resolveSubtype(ResolvedType, Class)
70+
*/
71+
public final ResolvedType resolveSubtype(ResolvedType supertype, Class<?> subtype) {
72+
return this.typeResolver.resolveSubtype(supertype, subtype);
73+
}
74+
6375
/**
6476
* Collect a given type's declared fields and methods.
6577
*

src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ protected void traverseGenericType(ResolvedType targetType, ObjectNode targetNod
204204

205205
/**
206206
* Preparation Step: add the given targetType. Also catering for forced inline-definitions and ignoring custom definitions
207-
*
207+
*
208208
* @param targetType (possibly generic) type to add
209209
* @param targetNode node in the JSON schema that should represent the targetType
210210
* @param isNullable whether the field/method's return value is allowed to be null in the declaringType in this particular scenario
@@ -220,6 +220,7 @@ private void traverseGenericType(ResolvedType targetType, ObjectNode targetNode,
220220
return;
221221
}
222222
final ObjectNode definition;
223+
boolean includeTypeAttributes = true;
223224
final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(targetType, this, ignoredDefinitionProvider);
224225
if (customDefinition != null && customDefinition.isMeantToBeInline()) {
225226
if (targetNode == null) {
@@ -256,21 +257,52 @@ private void traverseGenericType(ResolvedType targetType, ObjectNode targetNode,
256257
this.generateArrayDefinition(targetType, definition, isNullable);
257258
} else {
258259
logger.debug("generating definition for {}", targetType);
259-
this.generateObjectDefinition(targetType, definition);
260+
includeTypeAttributes = !this.addSubtypeReferencesInDefinition(targetType, definition);
260261
}
261262
}
262263
TypeScope scope = this.typeContext.createTypeScope(targetType);
263-
Set<String> allowedSchemaTypes = this.collectAllowedSchemaTypes(definition);
264-
ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes);
265-
// ensure no existing attributes in the 'definition' are replaced, by way of first overriding any conflicts the other way around
266-
typeAttributes.setAll(definition);
267-
// apply merged attributes
268-
definition.setAll(typeAttributes);
264+
if (includeTypeAttributes) {
265+
Set<String> allowedSchemaTypes = this.collectAllowedSchemaTypes(definition);
266+
ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes);
267+
// ensure no existing attributes in the 'definition' are replaced, by way of first overriding any conflicts the other way around
268+
typeAttributes.setAll(definition);
269+
// apply merged attributes
270+
definition.setAll(typeAttributes);
271+
}
269272
// apply overrides as the very last step
270273
this.generatorConfig.getTypeAttributeOverrides()
271274
.forEach(override -> override.overrideTypeAttributes(definition, scope, this.generatorConfig));
272275
}
273276

277+
/**
278+
* Check for any defined subtypes of the targeted java type to produce a definition for. If there are any configured subtypes, reference those
279+
* from within the definition being generated.
280+
*
281+
* @param targetType (possibly generic) type to add
282+
* @param definition node in the JSON schema to which all collected attributes should be added
283+
* @return whether any subtypes were found for which references were added to the given definition
284+
*/
285+
private boolean addSubtypeReferencesInDefinition(ResolvedType targetType, ObjectNode definition) {
286+
List<ResolvedType> subtypes = this.generatorConfig.resolveSubtypes(targetType, this);
287+
if (subtypes.isEmpty()) {
288+
this.generateObjectDefinition(targetType, definition);
289+
return false;
290+
}
291+
if (subtypes.size() == 1) {
292+
// avoid unnecessary "anyOf" by making the definition a direct reference to the subtype's definition
293+
this.traverseGenericType(subtypes.get(0), definition, false);
294+
} else {
295+
ArrayNode anyOfArrayNode = this.generatorConfig.createArrayNode();
296+
for (ResolvedType subtype : subtypes) {
297+
ObjectNode subtypeSchema = this.generatorConfig.createObjectNode();
298+
this.traverseGenericType(subtype, subtypeSchema, false);
299+
anyOfArrayNode.add(subtypeSchema);
300+
}
301+
definition.set(SchemaConstants.TAG_ANYOF, anyOfArrayNode);
302+
}
303+
return true;
304+
}
305+
274306
/**
275307
* Collect the specified value(s) from the given definition's "{@value SchemaConstants#TAG_TYPE}" attribute.
276308
*

0 commit comments

Comments
 (0)