Introduction
The victools/jsonschema-generator repository is home to multiple artifacts that are published independently to "The Central Repository" (Sonatype) and from there to others like "Maven Central". Besides the main generator library itself, there are a few modules providing some standard configurations for your convenience.
This documentation aims at always covering the latest released version of the jsonschema-generator
and its (standard) modules. There is no documentation for previous versions at this stage.
Please refer to the CHANGELOG for a list of the incremental changes.
The victools:jsonschema-generator aims at allowing the generation of JSON Schema (Draft 6, Draft 7, Draft 2019-09 or Draft 2020-12) to document Java code. This is expressly not limited to JSON but also allows for a Java API to be documented (i.e. including methods and the associated return values).
Generator – Options
The schema generation caters for a certain degree of flexibility out-of-the-box.
Various aspects can be toggled on/off by including or excluding respective Option
s.
configBuilder.with(
Option.EXTRA_OPEN_API_FORMAT_VALUES,
Option.PLAIN_DEFINITION_KEYS);
configBuilder.without(
Option.Schema_VERSION_INDICATOR,
Option.ENUM_KEYWORD_FOR_SINGLE_VALUES);
# | Behavior if included | Behavior if excluded |
---|---|---|
1 | Option.SCHEMA_VERSION_INDICATOR |
|
Setting appropriate $schema attribute on main schema being generated. |
No $schema attribute is being added. |
|
2 | Option.ADDITIONAL_FIXED_TYPES |
|
|
|
|
3 | Option.STANDARD_FORMATS |
|
Subset of the "format" values being added by Option.EXTRA_OPEN_API_FORMAT_VALUES but limited to built-in supported "format" values ("date" , "time" , "date-time" , "duration" , "uuid" , "uri" ).
Only works if Option.ADDITIONAL_FIXED_TYPES is set. and it is overriden by Option.EXTRA_OPEN_API_FORMAT_VALUES |
no automatic "format" values are being included, unless Option.EXTRA_OPEN_API_FORMAT_VALUES indicates otherwise. |
|
4 | Option.EXTRA_OPEN_API_FORMAT_VALUES |
|
Include extra "format" values (e.g. "int32" , "int64" , "date" , "time" , "date-time" , "duration" , "uuid" , "uri" ) for fixed types (primitive/basic types, plus some of the Option.ADDITIONAL_FIXED_TYPES if they are enabled as well). It overrides Option.STANDARD_FORMATS , which would cover only a subset of the OpenAPI format values. |
no automatic "format" values are being included, unless Option.STANDARD_FORMATS indicates otherwise. |
|
# | Behavior if included | Behavior if excluded |
5 | Option.SIMPLIFIED_ENUMS |
|
Treating encountered enum types as objects, but including only the name() method and listing the names of the enum constants as its enum values. |
- | |
6 | Option.FLATTENED_ENUMS |
|
Treating encountered enum types as { "type": "string" } schema with the names of the enum constants being listed as its enum values. |
- | |
7 | Option.FLATTENED_ENUMS_FROM_TOSTRING |
|
Treating encountered enum types as { "type": "string" } schema with the toString() values of the enum constants being listed as its enum values. |
- | |
8 | Option.SIMPLIFIED_OPTIONALS |
|
Treating encountered Optional instances as objects, but including only the get() , orElse() and isPresent() methods. |
- | |
# | Behavior if included | Behavior if excluded |
9 | Option.FLATTENED_OPTIONALS |
|
Replacing encountered Optional instances as null-able forms of their generic parameter type. |
- | |
10 | Option.FLATTENED_SUPPLIERS |
|
Replacing encountered Supplier instances with their generic parameter type. |
- | |
11 | Option.VALUES_FROM_CONSTANT_FIELDS |
|
Attempt to load the values of static final fields, serialize them via the ObjectMapper and include them as the respective schema's const value.
For this option to take effect, those
static final fields need to be included via Option.PUBLIC_STATIC_FIELDS and/or Option.NONPUBLIC_STATIC_FIELDS . |
No const values are populated for static final fields. |
|
12 | Option.PUBLIC_STATIC_FIELDS |
|
Include public static fields in an object's properties . |
No public static fields are included in an object's properties . |
|
# | Behavior if included | Behavior if excluded |
13 | Option.PUBLIC_NONSTATIC_FIELDS |
|
Include public non-static fields in an object's properties . |
No public non-static fields are included in an object's properties . |
|
14 | Option.NONPUBLIC_STATIC_FIELDS |
|
Include protected /package-visible/private static fields in an object's properties . |
No protected /package-visible/private static fields are included in an object's properties . |
|
15 | Option.NONPUBLIC_NONSTATIC_FIELDS_WITH_GETTERS |
|
Include protected /package-visible/private non-static fields in an object's properties if they have corresponding getter methods. |
No protected /package-visible/private non-static fields with getter methods are included in an object's properties . |
|
16 | Option.NONPUBLIC_NONSTATIC_FIELDS_WITHOUT_GETTERS |
|
Include protected /package-visible/private non-static fields in an object's properties if they don't have corresponding getter methods. |
No protected /package-visible/private non-static fields without getter methods are included in an object's properties . |
|
# | Behavior if included | Behavior if excluded |
17 | Option.TRANSIENT_FIELDS |
|
Include transient fields in an object's properties if they would otherwise be included according to the Option s above. |
No transient fields are included in an object's properties even if they would otherwise be included according to the Option s above. |
|
18 | Option.STATIC_METHODS |
|
Include public static methods in an object's properties |
No static methods are included in an object's properties even if they would be included according to the Option.VOID_METHODS below. |
|
19 | Option.VOID_METHODS |
|
Include public void methods in an object's properties |
No void methods are included in an object's properties even if they would be included according to the Option.STATIC_METHODS above. |
|
20 | Option.GETTER_METHODS |
|
Include public methods in an object's properties if a corresponding field exists that fulfills the usual naming conventions (getX() /x or isValid() /valid ). |
No methods are included in an object's properties > for which a field exists that fulfills the usual naming conventions. |
|
# | Behavior if included | Behavior if excluded |
21 | Option.NONSTATIC_NONVOID_NONGETTER_METHODS |
|
Include public non-static non-void methods in an object's properties for which no field exists that fulfills the usual getter naming conventions. |
No non-static /non-void /non-getter methods are included in an object's properties . |
|
22 | Option.NULLABLE_FIELDS_BY_DEFAULT |
|
The schema type for a field allows null by default unless some configuration specifically says it is not null-able. |
The schema type for a field does not allow for null by default unless some configuration specifically says it is null-able. |
|
23 | Option.NULLABLE_METHOD_RETURN_VALUES_BY_DEFAULT |
|
The schema type for a method's return type allows null by default unless some configuration specifically says it is not null-able. |
The schema type for a method's return type does not allow for null by default unless some configuration specifically says it is null-able. |
|
24 | Option.NULLABLE_ARRAY_ITEMS_ALLOWED |
|
The schema type for the items in an array (in case of a field's value or method's return value being a container/array) allows null , if the corresponding configuration explicitly says so. Otherwise, they're still deemed not null-able by default. |
The schema type for the items in an array (in case of a field's value or method's return value being a container/array) never allows null . |
|
# | Behavior if included | Behavior if excluded |
25 | Option.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS |
|
Include argument-free methods as fields, e.g. the return type of getName() will be included as name field. |
Argument-free methods will be included with the appended parentheses. | |
26 | Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES |
|
Setting the additionalProperties attribute in each Map<K, V> to a schema representing the declared value type V . |
Omitting the additionalProperties attribute in Map<K, V> schemas by default (thereby allowing additional properties of any type) unless some configuration specifically says something else. |
|
27 | Option.ACCEPT_SINGLE_VALUE_AS_ARRAY |
|
Including an anyOf for every field/method declaring a container type. The anyOf then allows for either the array as declared or just a single item type instead. |
A container type will be represented by an array with the declared item type. | |
28 | Option.ENUM_KEYWORD_FOR_SINGLE_VALUES |
|
Using the enum keyword for allowed values, even if there is only one. |
In case of a single allowed value, use the const keyword instead of enum . |
|
# | Behavior if included | Behavior if excluded |
29 | Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT |
|
Setting the additionalProperties attribute in all object schemas to false by default unless some configuration specifically says something else. |
Omitting the additionalProperties attribute in all object schemas by default (thereby allowing any additional properties) unless some configuration specifically says something else. |
|
30 | Option.DEFINITIONS_FOR_ALL_OBJECTS |
|
Include an entry in the $defs /definitions for each encountered object type that is not explicitly declared as "inline" via a custom definition. |
Only include those entries in the $defs /definitions for object types that are referenced more than once and which are not explicitly declared as "inline" via a custom definition. |
|
31 | Option.DEFINITION_FOR_MAIN_SCHEMA |
|
Include an entry in the $defs /definitions for the main/target type and a corresponding $ref on the top level (which is only valid from Draft 2019-09 onward). |
Define the main/target type "inline". | |
32 | Option.DEFINITIONS_FOR_MEMBER_SUPERTYPES |
|
For a member (field/method), having a declared type for which subtypes are being detected, include a single definition with any collected member attributes assigned directly. Any subtypes are only being handled as generic types, i.e., outside of the member context. That means, certain relevant annotations may be ignored (e.g. a jackson @JsonTypeInfo override on a single member would not be correctly reflected in the produced schema). |
For a member (field/method), having a declared type for which subtypes are being detected, include a list of definittions: one for each subtype in the given member's context. This allows independently interpreting contextual information (e.g., member annotations) for each subtype. | |
# | Behavior if included | Behavior if excluded |
33 | Option.INLINE_ALL_SCHEMAS |
|
Do not include any $defs /definitions but rather define all sub-schemas "inline" – however, this results in an exception being thrown if the given type contains any kind of circular reference. |
Depending on whether DEFINITIONS_FOR_ALL_OBJECTS is included or excluded. |
|
34 | Option.INLINE_NULLABLE_SCHEMAS |
|
Do not include $defs /definitions for a nullable version of a type, but rather define it "inline". The non-nullable type may still be referenced. |
Depending on whether DEFINITIONS_FOR_ALL_OBJECTS is included or excluded. |
|
35 | Option.PLAIN_DEFINITION_KEYS |
|
Ensure that the keys for any $defs /definitions match the regular expression ^[a-zA-Z0-9\.\-_]+$ (as expected by the OpenAPI specification 3.0). |
Ensure that the keys for any $defs /definitions are URI compatible (as expected by the JSON Schema specification). |
|
36 | Option.ALLOF_CLEANUP_AT_THE_END |
|
At the very end of the schema generation reduce allOf wrappers where it is possible without overwriting any attributes – this also affects the results from custom definitions. |
Do not attempt to reduce allOf wrappers but preserve them as they were generated regardless of them being necessary or not. |
|
# | Behavior if included | Behavior if excluded |
37 | Option.STRICT_TYPE_INFO |
|
As final step in the schema generation process, ensure all sub schemas containing keywords implying a particular "type" (e.g., "properties" implying an "object") have this "type" declared explicitly – this also affects the results from custom definitions. | No additional "type" indication will be added for each sub schema, e.g. on the collected attributes where the "allOf" clean-up could not be applied or was disabled. | |
38 | Option.NULLABLE_ALWAYS_AS_ANYOF |
|
A "type": "null" will not be combined with other "type" values in an array. Instead, a separate "anyOf" with a subschema only containing the "type": "null" will be included. | For brevity's sake, a "type": "null" may be combined with other "type" values, e.g. as "type": ["null", "object"]. |
Below, you can find the lists of Option
s included/excluded in the respective standard OptionPreset
s:
- "F_D" =
FULL_DOCUMENTATION
- "J_O" =
JAVA_OBJECT
- "P_J" =
PLAIN_JSON
# | Standard Option |
F_D | J_O | P_J |
---|---|---|---|---|
1 | SCHEMA_VERSION_INDICATOR |
⬜️ | ⬜️ | ✅ |
2 | ADDITIONAL_FIXED_TYPES |
⬜️ | ⬜️ | ✅ |
3 | STANDARD_FORMATS |
⬜ | ⬜️ | ✅ |
4 | EXTRA_OPEN_API_FORMAT_VALUES |
⬜️ | ⬜️ | ⬜️ |
5 | SIMPLIFIED_ENUMS |
✅ | ✅ | ⬜️ |
6 | FLATTENED_ENUMS |
⬜️ | ⬜️ | ✅ |
7 | FLATTENED_ENUMS_FROM_TOSTRING |
⬜️ | ⬜️ | ⬜️ |
8 | SIMPLIFIED_OPTIONALS |
✅ | ✅ | ⬜️ |
9 | FLATTENED_OPTIONALS |
⬜️ | ⬜️ | ✅ |
10 | FLATTENED_SUPPLIERS |
⬜️ | ⬜️ | ✅ |
11 | VALUES_FROM_CONSTANT_FIELDS |
✅ | ✅ | ✅ |
12 | PUBLIC_STATIC_FIELDS |
✅ | ✅ | ⬜️ |
13 | PUBLIC_NONSTATIC_FIELDS |
✅ | ✅ | ✅ |
14 | NONPUBLIC_STATIC_FIELDS |
✅ | ⬜️ | ⬜️ |
15 | NONPUBLIC_NONSTATIC_FIELDS_WITH_GETTERS |
✅ | ⬜️ | ✅ |
16 | NONPUBLIC_NONSTATIC_FIELDS_WITHOUT_GETTERS |
✅ | ⬜️ | ✅ |
17 | TRANSIENT_FIELDS |
✅ | ⬜️ | ⬜️ |
18 | STATIC_METHODS |
✅ | ✅ | ⬜️ |
19 | VOID_METHODS |
✅ | ✅ | ⬜️ |
20 | GETTER_METHODS |
✅ | ✅ | ⬜️ |
21 | NONSTATIC_NONVOID_NONGETTER_METHODS |
✅ | ✅ | ⬜️ |
22 | NULLABLE_FIELDS_BY_DEFAULT |
✅ | ⬜️ | ⬜️ |
23 | NULLABLE_METHOD_RETURN_VALUES_BY_DEFAULT |
✅ | ⬜️ | ⬜️ |
24 | NULLABLE_ARRAY_ITEMS_ALLOWED |
⬜️ | ⬜️ | ⬜️ |
25 | FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS |
⬜️ | ⬜️ | ⬜️ |
26 | MAP_VALUES_AS_ADDITIONAL_PROPERTIES |
⬜️ | ⬜️ | ⬜️ |
27 | ACCEPT_SINGLE_VALUE_AS_ARRAY |
⬜️ | ⬜️ | ⬜️ |
28 | ENUM_KEYWORD_FOR_SINGLE_VALUES |
⬜️ | ⬜️ | ⬜️ |
29 | FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT |
⬜️ | ⬜️ | ⬜️ |
30 | DEFINITIONS_FOR_ALL_OBJECTS |
⬜️ | ⬜️ | ⬜️ |
31 | DEFINITION_FOR_MAIN_SCHEMA |
⬜️ | ⬜️ | ⬜️ |
32 | DEFINITIONS_FOR_MEMBER_SUPERTYPES |
⬜️ | ⬜️ | ⬜️ |
33 | INLINE_ALL_SCHEMAS |
⬜️ | ⬜️ | ⬜️ |
34 | INLINE_NULLABLE_SCHEMAS |
⬜️ | ⬜️ | ⬜️ |
35 | PLAIN_DEFINITION_KEYS |
⬜️ | ⬜️ | ⬜️ |
36 | ALLOF_CLEANUP_AT_THE_END |
✅ | ✅ | ✅ |
37 | STRICT_TYPE_INFO |
⬜️ | ⬜️ | ⬜️ |
38 | NULLABLE_ALWAYS_AS_ANYOF |
⬜️ | ⬜️ | ⬜️ |
Generator – Modules
Similar to an OptionPreset
being a short-cut to including various Option
s, the concept of Module
s is a convenient way of including multiple individual configurations or even advanced configurations (as per the following sections) at once.
You can easily group your own set of configurations into a Module
if you wish.
However, the main intention behind Module
s is that they are an entry-point for separate external dependencies you can "plug-in" as required via SchemaGeneratorConfigBuilder.with(Module)
, like the few standard Module
s documented below.
Generator – Individual Configurations
E.g. for the given configuration:
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09);
configBuilder.forField()
.withTitleResolver(field -> field.getName() + " = "
+ (field.isFakeContainerItemScope() ? "(fake) " : "(real) ")
+ field.getSimpleTypeDescription())
.withDescriptionResolver(field -> "original type = "
+ field.getContext().getSimpleTypeDescription(field.getDeclaredType()));
JsonNode mySchema = new SchemaGenerator(configBuilder.build())
.generateSchema(MyClass.class);
and target class:
class MyClass {
public List<String> texts;
}
The following schema will be generated:
{
"type": "object",
"properties": {
"texts": {
"type": "array",
"title": "texts = (real) List<String>",
"description": "original type = List<String>",
"items": {
"type": "string",
"title": "texts = (fake) String",
"description": "original type = List<String>"
}
}
}
}
In order to control various attributes being set during the schema generation, you can define for each (supported) one of them individually how a respective value should be resolved. Overall, you usually have the same configuration options either for:
- an encountered type in general via
SchemaGeneratorConfigBuilder.forTypesInGeneral()
or - in the context of a specific field via
SchemaGeneratorConfigBuilder.forFields()
or - in the context of a specific method's return value via
SchemaGeneratorConfigBuilder.forMethods()
.
The jsonschema-generator README contains a list of the supported JSON Schema attributes.
The following list of individual configuration options on the SchemaGeneratorConfigBuilder
is to a large extent the inverse of that list.
"$id"
Keyword
configBuilder.forTypesInGeneral()
.withIdResolver(scope -> scope.getType().getErasedType() == MyClass.class ? "main-schema-id" : null);
withIdResolver()
is expecting the "$id"
attribute's value to be returned based on a given TypeScope
– in case of multiple configurations, the first non-null
value will be applied.
"$anchor"
Keyword
configBuilder.forTypesInGeneral()
.withAnchorResolver(scope -> scope.getType().getErasedType() == AnchorClass.class ? "anchor-value" : null);
withAnchorResolver()
is expecting the "$anchor"
attribute's value to be returned based on a given TypeScope
– in case of multiple configurations, the first non-null
value will be applied.
Order of entries in "properties"
Keyword
configBuilder.forTypesInGeneral()
.withPropertySorter(PropertySortUtils.SORT_PROPERTIES_FIELDS_BEFORE_METHODS
.thenComparing((memberOne, memberTwo) ->
// sort fields/methods alphabetically, while ignoring upper/lower case
memberOne.getSchemaPropertyName().toLowerCase()
.compareTo(memberTwo.getSchemaPropertyName().toLowerCase()));
withPropertySorter()
is expecting a Comparator
for sorting an object's fields and methods in the produced "properties"
– this replaces any previously given sorting algorithm, i.e. only one Comparator
can be set – by default, fields are listed before methods with each group in alphabetical order.
Names in global "$defs"
/"definitions"
configBuilder.forTypesInGeneral()
.withDefinitionNamingStrategy(new DefaultSchemaDefinitionNamingStrategy() {
@Override
public String getDefinitionNameForKey(DefinitionKey key, SchemaGenerationContext context) {
return super.getDefinitionNameForKey(key, generationContext).toLowerCase();
}
@Override
public void adjustDuplicateNames(Map<DefinitionKey, String> duplicateNames, SchemaGenerationContext context) {
char suffix = 'a';
duplicateNames.entrySet().forEach(entry -> entry.setValue(entry.getValue() + "-" + suffix++));
}
@Override
public String adjustNullableName(DefinitionKey key, String definitionName, SchemaGenerationContext context) {
return definitionName + "-nullable";
}
});
withDefinitionNamingStrategy()
is expecting a SchemaDefinitionNamingStrategy
that defines what keys to assign to subschemas in the "definitions"
/"$defs"
.
Optionally, you can override the logic how to adjust them in case of multiple types having the same name and for a subschema's nullable alternative.
There is a DefaultSchemaDefinitionNamingStrategy
, which is being applied if you don't set a specific naming strategy yourself:
- It uses a given type's simple class name (i.e. without package prefix) as the definition name, potentially prepending type arguments in case of it being a parameterized type.
- Duplicate names may occur if the same simple class name (with identical type parameters) appears multiple times in your schema, i.e. from different packages. As the definition names need to be unique, those are then prepended with a running number. E.g.
java.time.DateTime
andyour.pkg.DateTime
would be represented byDateTime-1
andDateTime-2
. - When a given type appears in its
null
able and non-null
able form, two separate definitions may be included to reduce duplication. The "normal" named one and thenull
able one getting a"-nullable"
suffix to its definition name.
Names of fields/methods in an object's properties
configBuilder.forFields()
.withPropertyNameOverrideResolver(field -> Optional
.ofNullable(field.getAnnotationConsideringFieldAndGetter(JsonProperty.class))
.map(JsonProperty::value).orElse(null));
configBuilder.forMethods()
.withPropertyNameOverrideResolver(method -> method.getName().startsWith("is") && method.getArgumentCount() == 0
? method.getName().substring(2, method.getName().length() - 2) : null);
withPropertyNameOverrideResolver()
is expecting an alternative name to be returned for a given FieldScope
/MethodScope
to be used as key in the containing object's "properties"
– the first non-null
value will be applied.
Omitting/ignoring certain fields/methods
configBuilder.forFields()
.withIgnoreCheck(field -> field.getName().startsWith("_"));
configBuilder.forMethods()
.withIgnoreCheck(method -> !method.isVoid() && method.getType().getErasedType() == Object.class);
withIgnoreCheck()
is expecting the indication to be returned whether a given FieldScope
/MethodScope
should be excluded from the generated schema. If any check returns true
, the field/method will be ignored.
Decide whether a field's/method's value may be null
configBuilder.forFields()
.withNullableCheck(field -> field.getAnnotationConsideringFieldAndGetter(Nullable.class) != null);
configBuilder.forMethods()
.withNullableCheck(method -> method.getAnnotationConsideringFieldAndGetter(NotNull.class) == null);
withNullableCheck()
is expecting the indication to be returned whether a given FieldScope
/MethodScope
may return null
and should therefore include "null"
in the generated schema's "type"
.
- If there is no check or all of them return
null
, the default will be applied (depending on whetherOption.NULLABLE_FIELDS_BY_DEFAULT
/Option.NULLABLE_METHOD_RETURN_VALUES_BY_DEFAULT
were enabled). - If any check returns
true
, the field/method will be deemed nullable. - Otherwise, the field/method will be deemed not-nullable.
"required"
Keyword
configBuilder.forFields()
.withRequiredCheck(field -> field.getAnnotationConsideringFieldAndGetter(Nullable.class) == null);
configBuilder.forMethods()
.withRequiredCheck(method -> method.getAnnotationConsideringFieldAndGetter(NotNull.class) != null);
withRequiredCheck()
is expecting the indication to be returned whether a given FieldScope
/MethodScope
should be included in the "required"
attribute – if any check returns true
, the field/method will be deemed "required"
.
"dependentRequired"
Keyword
configBuilder.forFields()
.withDependentRequiresResolver(field -> Optional
.ofNullable(field.getAnnotationConsideringFieldAndGetter(IfPresentAlsoRequire.class))
.map(IfPresentAlsoRequire::value)
.map(Arrays::asList)
.orElse(null));
configBuilder.forMethods()
.withDependentRequiresResolver(method -> Optional.ofNullable(method.findGetterField())
.map(FieldScope::getSchemaPropertyName)
.map(Collections::singletonList)
.orElse(null));
withDependentRequiresResolver()
is expecting the names of other properties to be returned, which should be deemed "required", if the property represented by the given field/method is present.
The results of all registered resolvers are being combined.
"readOnly"
Keyword
configBuilder.forFields()
.withReadOnlyCheck(field -> field.getAnnotationConsideringFieldAndGetter(ReadOnly.class) != null);
configBuilder.forMethods()
.withReadOnlyCheck(method -> method.getAnnotationConsideringFieldAndGetter(ReadOnly.class) != null);
withReadOnlyCheck()
is expecting the indication to be returned whether a given FieldScope
/MethodScope
should be included in the "readOnly"
attribute – if any check returns true
, the field/method will be deemed "readOnly"
.
"writeOnly"
Keyword
configBuilder.forFields()
.withWriteOnlyCheck(field -> field.getAnnotationConsideringFieldAndGetter(WriteOnly.class) != null);
configBuilder.forMethods()
.withWriteOnlyCheck(method -> method.getAnnotationConsideringFieldAndGetter(WriteOnly.class) != null);
withWriteOnlyCheck()
is expecting the indication to be returned whether a given FieldScope
/MethodScope
should be included in the "writeOnly"
attribute – if any check returns true
, the field/method will be deemed "writeOnly"
.
"title"
Keyword
configBuilder.forTypesInGeneral()
.withTitleResolver(scope -> scope.getType().getErasedType() == YourClass.class ? "main schema title" : null);
configBuilder.forFields()
.withTitleResolver(field -> field.getType().getErasedType() == String.class ? "text field" : null);
configBuilder.forMethods()
.withTitleResolver(method -> method.getName().startsWith("get") ? "getter" : null);
withTitleResolver()
is expecting the "title"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"description"
Keyword
configBuilder.forTypesInGeneral()
.withDescriptionResolver(scope -> scope.getType().getErasedType() == YourClass.class ? "main schema description" : null);
configBuilder.forFields()
.withDescriptionResolver(field -> field.getType().getErasedType() == String.class ? "text field" : null);
configBuilder.forMethods()
.withDescriptionResolver(method -> method.getName().startsWith("get") ? "getter" : null);
withDescriptionResolver()
is expecting the "description"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"default"
Keyword
configBuilder.forTypesInGeneral()
.withDefaultResolver(scope -> scope.getType().getErasedType() == boolean.class ? Boolean.FALSE : null);
configBuilder.forFields()
.withDefaultResolver(field -> field.getType().getErasedType() == String.class ? "" : null);
configBuilder.forMethods()
.withDefaultResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetter(Default.class))
.map(Default::value).orElse(null));
withDefaultResolver()
is expecting the "default"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied, which will be serialised through the ObjectMapper
instance provided in the SchemaGeneratorConfigBuilder
's constructor.
"const"
/"enum"
Keyword
configBuilder.forTypesInGeneral()
.withEnumResolver(scope -> scope.getType().getErasedType().isEnum()
? Stream.of(scope.getType().getErasedType().getEnumConstants())
.map(v -> ((Enum) v).name()).collect(Collectors.toList())
: null);
configBuilder.forFields()
.withEnumResolver(field -> Optional
.ofNullable(field.getAnnotationConsideringFieldAndGetter(AllowedValues.class))
.map(AllowedValues::valueList).orElse(null));
configBuilder.forMethods()
.withEnumResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetter(SupportedValues.class))
.map(SupportedValues::values).map(Arrays::asList).orElse(null));
withEnumResolver()
is expecting the "const"
/"enum"
attribute's value(s) to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied, which will be serialised through the ObjectMapper
instance provided in the SchemaGeneratorConfigBuilder
's constructor.
"additionalProperties"
Keyword
Option 1: derive plain type from given scope
One version of the withAdditionalPropertiesResolver()
is expecting the "additionalProperties"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
configBuilder.forTypesInGeneral()
.withAdditionalPropertiesResolver(scope -> Object.class);
configBuilder.forFields()
.withAdditionalPropertiesResolver(field -> field.getType().getErasedType() == Object.class
? null : Void.class);
configBuilder.forMethods()
.withAdditionalPropertiesResolver(method -> method.getType().getErasedType() == Map.class
? method.getTypeParameterFor(Map.class, 1) : Void.class);
- If
null
is being returned, the next registeredAdditionalPropertiesResolver
will be checked. If all returnnull
, the attribute will be omitted. - If
Object.class
is being returned, the"additionalProperties"
attribute will be omitted. - if
Void.class
is being returned, the"additionalProperties"
will be set tofalse
. - If any other type is being returned (e.g. other
Class
or aResolvedType
) a corresponding schema will be included in"additionalProperties"
.
Option 2: specify explicit subschema
Another version of the withAdditionalPropertiesResolver()
is expecting the "additionalProperties"
attribute's value to be provided directly as a JsonNode
(e.g., ObjectNode
) representing the desired subschema.
In this case, both the TypeScope
/FieldScope
/MethodScope
and the overall generation context are being provided as input parameters.
configBuilder.forTypesInGeneral()
.withAdditionalPropertiesResolver((scope, context) -> BooleanNode.TRUE);
configBuilder.forFields()
.withAdditionalPropertiesResolver((field, context) -> field.getType().getErasedType() == Object.class
? null : BooleanNode.FALSE);
configBuilder.forMethods()
.withAdditionalPropertiesResolver((method, context) -> {
if (!method.getType().isInstanceOf(Map.class)) {
return null;
}
ResolvedType valueType = method.getTypeParameterFor(Map.class, 1);
if (valueType == null || valueType.getErasedType() == Object.class) {
return null;
}
return context.createStandardDefinitionReference(method.asFakeContainerItemScope(Map.class, 1), null);
});
- If
null
is being returned, the next registeredAdditionalPropertiesResolver
will be checked. If all returnnull
, the attribute will be omitted. - If
BooleanNode.TRUE
is being returned, the"additionalProperties"
attribute will be omitted. - if
BooleanNode.FALSE
is being returned, the"additionalProperties"
will be set tofalse
. - If any other subschema is being returned, that will be included as
"additionalProperties"
attribute directly.
This usage of the FieldScope
/MethodScope
potentially via asFakeContainerItemScope()
has the advantage of allowing the consideration of annotations on generic parameters, such as the one on Map<String, @Min(10) Integer>
when that is the declared type of a field/method.
"patternProperties"
Keyword
Option 1: derive plain types from given scope
One version of the withPatternPropertiesResolver()
is expecting a Map
of regular expressions to their corresponding allowed types to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
configBuilder.forTypesInGeneral()
.withPatternPropertiesResolver(scope -> scope.getType().isInstanceOf(Map.class)
? Collections.singletonMap("^[a-zA-Z]+$", scope.getTypeParameterFor(Map.class, 1)) : null);
configBuilder.forFields()
.withPatternPropertiesResolver(field -> field.getType().isInstanceOf(TypedMap.class)
? Collections.singletonMap("_int$", int.class) : null);
configBuilder.forMethods()
.withPatternPropertiesResolver(method -> method.getType().isInstanceOf(StringMap.class)
? Collections.singletonMap("^txt_", String.class) : null);
Each regular expression will be included as key in the "patternProperties"
attribute with a schema representing the mapped type as the corresponding value.
Option 2: specify explicit subschema
Another version of the withPatternPropertiesResolver()
is expecting a Map
with each value being a JsonNode
(e.g., ObjectNode
) representing the respective desired subschema.
In this case, both the TypeScope
/FieldScope
/MethodScope
and the overall generation context are being provided as input parameters.
The generation of the subschema could look similar to the example given for the
"additionalProperties"
attribute above.
The usage of the FieldScope
/MethodScope
potentially via asFakeContainerItemScope()
has the advantage of allowing the consideration of annotations on generic parameters, such as the one on Map<String, @Min(10) Integer>
when that is the declared type of a field/method.
"minLength"
Keyword
configBuilder.forTypesInGeneral()
.withStringMinLengthResolver(scope -> scope.getType().getErasedType() == UUID.class ? 36 : null);
configBuilder.forFields()
.withStringMinLengthResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(NotEmpty.class) == null ? null : 1);
configBuilder.forMethods()
.withStringMinLengthResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Size.class))
.map(Size::min).orElse(null));
withStringMinLengthResolver()
is expecting the "minLength"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"maxLength"
Keyword
configBuilder.forTypesInGeneral()
.withStringMaxLengthResolver(scope -> scope.getType().getErasedType() == UUID.class ? 36 : null);
configBuilder.forFields()
.withStringMaxLengthResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(DbKey.class) == null ? null : 450);
configBuilder.forMethods()
.withStringMaxLengthResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Size.class))
.map(Size::max).orElse(null));
withStringMaxLengthResolver()
is expecting the "maxLength"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"format"
Keyword
configBuilder.forTypesInGeneral()
.withStringFormatResolver(scope -> scope.getType().getErasedType() == UUID.class ? "uuid" : null);
configBuilder.forFields()
.withStringFormatResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(Email.class) == null ? null : "email");
configBuilder.forMethods()
.withStringFormatResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Schema.class))
.map(Schema::format).orElse(null));
withStringFormatResolver()
is expecting the "format"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"pattern"
Keyword
configBuilder.forTypesInGeneral()
.withStringPatternResolver(scope -> scope.getType().getErasedType() == UUID.class
? "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[89aAbB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$" : null);
configBuilder.forFields()
.withStringPatternResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(Email.class) == null ? null : "^.+@.+\\..+$");
configBuilder.forMethods()
.withStringPatternResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Pattern.class))
.map(Pattern::value).orElse(null));
withStringPatternResolver()
is expecting the "pattern"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"minimum"
Keyword
configBuilder.forTypesInGeneral()
.withNumberInclusiveMinimumResolver(scope -> scope.getType().getErasedType() == PositiveInt.class
? BigDecimal.ONE : null);
configBuilder.forFields()
.withNumberInclusiveMinimumResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(NonNegative.class) == null ? null : BigDecimal.ZERO);
configBuilder.forMethods()
.withNumberInclusiveMinimumResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Minimum.class))
.filter(a -> !a.exclusive()).map(Minimum::value).orElse(null));
withNumberInclusiveMinimumResolver()
is expecting the "minimum"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"exclusiveMinimum"
Keyword
configBuilder.forTypesInGeneral()
.withNumberExclusiveMinimumResolver(scope -> scope.getType().getErasedType() == PositiveDecimal.class
? BigDecimal.ZERO : null);
configBuilder.forFields()
.withNumberExclusiveMinimumResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(Positive.class) == null ? null : BigDecimal.ZERO);
configBuilder.forMethods()
.withNumberExclusiveMinimumResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Minimum.class))
.filter(Minimum::exclusive).map(Minimum::value).orElse(null));
withNumberExclusiveMinimumResolver()
is expecting the "exclusiveMinimum"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"maximum"
Keyword
configBuilder.forTypesInGeneral()
.withNumberInclusiveMaximumResolver(scope -> scope.getType().getErasedType() == int.class
? new BigDecimal(Integer.MAX_VALUE) : null);
configBuilder.forFields()
.withNumberInclusiveMaximumResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(NonPositive.class) == null ? null : BigDecimal.ZERO);
configBuilder.forMethods()
.withNumberInclusiveMaximumResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Maximum.class))
.filter(a -> !a.exclusive()).map(Maximum::value).orElse(null));
withNumberInclusiveMaximumResolver()
is expecting the "maximum"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"exclusiveMaximum"
Keyword
configBuilder.forTypesInGeneral()
.withNumberExclusiveMaximumResolver(scope -> scope.getType().getErasedType() == NegativeInt.class
? BigDecimal.ZERO : null);
configBuilder.forFields()
.withNumberExclusiveMaximumResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(Negative.class) == null ? null : BigDecimal.ZERO);
configBuilder.forMethods()
.withNumberExclusiveMaximumResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Maximum.class))
.filter(Maximum::exclusive).map(Maximum::value).orElse(null));
withNumberExclusiveMaximumResolver()
is expecting the "exclusiveMaximum"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"multipleOf"
Keyword
configBuilder.forTypesInGeneral()
.withNumberMultipleOfResolver(scope -> scope.getType().getErasedType() == int.class
? BigDecimal.ONE : null);
configBuilder.forFields()
.withNumberMultipleOfResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(Currency.class) == null ? null : new BigDecimal("0.01"));
configBuilder.forMethods()
.withNumberMultipleOfResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(NumericConstraint.class))
.map(NumericConstraint::multipleOf).orElse(null));
withNumberMultipleOfResolver()
is expecting the "multipleOf"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"minItems"
Keyword
configBuilder.forTypesInGeneral()
.withArrayMinItemsResolver(scope -> scope.getType().isInstanceOf(MandatoryList.class) ? 1 : null);
configBuilder.forFields()
.withArrayMinItemsResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(NotEmpty.class) == null ? null : 1);
configBuilder.forMethods()
.withArrayMinItemsResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Size.class))
.map(Size::min).orElse(null));
withArrayMinItemsResolver()
is expecting the "minItems"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"maxItems"
Keyword
configBuilder.forTypesInGeneral()
.withArrayMaxItemsResolver(scope -> scope.getType().isInstanceOf(Triple.class) ? 3 : null);
configBuilder.forFields()
.withArrayMaxItemsResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(NoMoreThanADozen.class) == null ? null : 12);
configBuilder.forMethods()
.withArrayMaxItemsResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(Size.class))
.map(Size::max).orElse(null));
withArrayMaxItemsResolver()
is expecting the "maxItems"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
"uniqueItems"
Keyword
configBuilder.forTypesInGeneral()
.withArrayUniqueItemsResolver(scope -> scope.getType().isInstanceOf(Set.class) ? true : null);
configBuilder.forFields()
.withArrayUniqueItemsResolver(field -> field
.getAnnotationConsideringFieldAndGetterIfSupported(Unique.class) == null ? null : true);
configBuilder.forMethods()
.withArrayUniqueItemsResolver(method -> Optional
.ofNullable(method.getAnnotationConsideringFieldAndGetterIfSupported(ListConstraints.class))
.map(ListConstraints::distinct).orElse(null));
withArrayUniqueItemsResolver()
is expecting the "uniqueItems"
attribute's value to be returned based on a given TypeScope
/FieldScope
/MethodScope
– the first non-null
value will be applied.
Generator – Advanced Configurations
When all of the above configuration options are insufficient to achieve your requirements, there are some more advanced configurations you can resort to.
Instance Attribute Overrides
configBuilder.forFields()
.withInstanceAttributeOverride((node, field, context) -> node
.put("$comment", "Field name in code: " + field.getDeclaredName()));
configBuilder.forMethods()
.withInstanceAttributeOverride((node, method, context) -> node
.put("readOnly", true));
If you want to set an attribute that is missing in the supported Individual Configurations for fields/methods or just want to have the last say in what combination of attribute values is being set for a field/method, you can use the following configurations:
SchemaGeneratorConfigBuilder.forFields().withInstanceAttributeOverride()
SchemaGeneratorConfigBuilder.forMethods().withInstanceAttributeOverride()
All defined overrides will be applied in the order of having been added to the SchemaGeneratorConfigBuilder
. Each receiving the then-current set of attributes on an ObjectNode
which can be freely manipulated.
Type Attribute Overrides
configBuilder.forTypesInGeneral()
.withTypeAttributeOverride((node, scope, context) -> node
.put("$comment", "Java type: " + scope.getType().getErasedType().getName()));
Similarly to (but not quite the same as) the Instance Attribute Overrides for fields/methods you can add missing attributes or manipulate collected ones on a per-type level through the following configuration:
SchemaGeneratorConfigBuilder.forTypesInGeneral().withTypeAttributeOverride()
All defined overrides will be applied in the order of having been added to the SchemaGeneratorConfigBuilder
.
Each receiving the then-current type definition including the collected set of attributes on an ObjectNode
which can be freely manipulated.
Target Type Overrides
E.g. for the
value
field in the following class you may know that the returned value is either aString
or aNumber
but there is no common supertype butObject
that can be declared:
class ExampleForTargetTypeOverrides {
@ValidOneOfTypes({String.class, Number.class})
private Object value;
public void setValue(String textValue) {
this.value = textValue;
}
public void setValue(Number numericValue) {
this.value = numericValue;
}
}
This could be solved by the following configuration:
configBuilder.forFields()
.withTargetTypeOverridesResolver(field -> Optional
.ofNullable(field.getAnnotationConsideringFieldAndGetterIfSupported(ValidOneOfTypes.class))
.map(ValidOneOfTypes::value).map(Stream::of)
.map(stream -> stream.map(specificSubtype -> field.getContext().resolve(specificSubtype)))
.map(stream -> stream.collect(Collectors.toList()))
.orElse(null));
The generated schema would look like this then:
{
"type": "object",
"properties": {
"value": {
"anyOf": [
{ "type": "string" },
{ "type": "number" }
]
}
}
}
Java does not support multiple type alternatives to be declared. This means you may have to declare a rather generic type on a field or as a method's return value even though there is only a finite list of types that you actually expect to be returned.
To improve the generated schema by listing the actual alternatives via "anyOf"
, you can make use of the following configurations:
SchemaGeneratorConfigBuilder.forFields().withTargetTypeOverridesResolver()
SchemaGeneratorConfigBuilder.forMethods().withTargetTypeOverridesResolver()
Subtype Resolvers
E.g. to replace every occurrence of the
Animal
interface with theCat
andDog
implementations:
configBuilder.forTypesInGeneral()
.withSubtypeResolver((declaredType, generationContext) -> {
if (declaredType.getErasedType() == Animal.class) {
TypeContext typeContext = generationContext.getTypeContext();
return Arrays.asList(
typeContext.resolveSubtype(declaredType, Cat.class),
typeContext.resolveSubtype(declaredType, Dog.class)
);
}
return null;
});
When a declared type is not too broad as in the example for Target Type Overrides above, but rather an appropriate supertype or interface. You may also want to list the alternative implementations via "anyOf"
wherever you encounter an abstract
class or interface.
In order to reflect Java's polymorphism, you can make use of the following configuration:
SchemaGeneratorConfigBuilder.forTypesInGeneral().withSubtypeResolver()
This can of course be more generalised by employing your reflections library of choice for scanning your classpath for all implementations of an encountered type.
Custom Type Definitions
E.g. treat
Collection
s as objects and not as"type": "array"
(which is the default):
configBuilder.forTypesInGeneral()
.withCustomDefinitionProvider((javaType, context) -> {
if (!javaType.isInstanceOf(Collection.class)) {
return null;
}
ResolvedType generic = context.getTypeContext().getContainerItemType(javaType);
SchemaGeneratorConfig config = context.getGeneratorConfig();
return new CustomDefinition(context.getGeneratorConfig().createObjectNode()
.put(config.getKeyword(SchemaKeyword.TAG_TYPE),
config.getKeyword(SchemaKeyword.TAG_TYPE_OBJECT))
.set(config.getKeyword(SchemaKeyword.TAG_PROPERTIES),
config.createObjectNode().set("stream().findFirst().orElse(null)",
context.makeNullable(context.createDefinitionReference(generic)))));
});
When all the generic configurations are not enough to achieve your specific requirements, you can still directly define parts of the schema yourself through the following configuration:
SchemaGeneratorConfigBuilder.forTypesInGeneral().withCustomDefinitionProvider()
(1) When including an unchanged schema of a different type, use
createDefinitionReference()
:
configBuilder.forTypesInGeneral()
.withCustomDefinitionProvider((javaType, context) ->
javaType.isInstanceOf(UUID.class)
? new CustomDefinition(context.createDefinitionReference(
context.getTypeContext().resolve(String.class)))
: null);
(2) When including an unchanged schema of the same type, use
createStandardDefinitionReference()
:
CustomDefinitionProviderV2 thisProvider = (javaType, context) ->
javaType.isInstanceOf(Collection.class)
? new CustomDefinition(
context.createStandardDefinitionReference(javaType, thisProvider),
DefinitionType.STANDARD, AttributeInclusion.NO)
: null;
configBuilder.forTypesInGeneral()
.withCustomDefinitionProvider(thisProvider);
(3) When adjusting a schema of a different type, use
createDefinition()
:
configBuilder.forTypesInGeneral()
.withCustomDefinitionProvider((javaType, context) ->
javaType.isInstanceOf(UUID.class)
? new CustomDefinition(context.createDefinition(
context.getTypeContext().resolve(String.class))
.put("format", "uuid"))
: null);
(4) When adjusting a schema of the same type, use
createStandardDefinition()
:
CustomDefinitionProviderV2 thisProvider = (javaType, context) ->
javaType.isInstanceOf(Collection.class)
? new CustomDefinition(
context.createStandardDefinition(javaType, thisProvider)
.put("$comment", "collection without other attributes"),
DefinitionType.STANDARD, AttributeInclusion.NO)
: null;
configBuilder.forTypesInGeneral()
.withCustomDefinitionProvider(thisProvider);
SchemaGenerationContext.createDefinitionReference()
creates a temporarily empty node which will be populated later with either a$ref
or the appropriate inline schema, i.e. in order to not produce an inline definition – thereby allowing you to avoid endless loops in case of circular references.SchemaGenerationContext.createStandardDefinitionReference()
to be used instead of the above when targeting the same type, to skip the current definition provider (and all previous ones) and thereby avoid endless loops.SchemaGenerationContext.createDefinition()
creates an inline definition of the given scope, allowing you to apply changes on top (similar to attribute overrides); thereby avoiding the need to manually create everything from scratch.SchemaGenerationContext.createStandardDefinition()
to be used instead of the above when targeting the same type, to skip the current definition provider (and all previous ones) and thereby avoid endless loops.
Other useful methods available in the context of a custom definition provider are:
SchemaGenerationContext.getGeneratorConfig().getObjectMapper().readTree()
allowing you to parse a string into a json (schema), in case you prefer to statically provide (parts of) the custom definitions.SchemaGenerationContext.getTypeContext().resolve()
allowing you to produceResolvedType
instances which are expected by various other methods.
Custom Property Definitions
// read a static schema string from an annotation
CustomPropertyDefinitionProvider provider = (member, context) -> Optional
.ofNullable(member.getAnnotationConsideringFieldAndGetter(Subschema.class))
.map(Subschema::value)
.map(rawSchema -> {
try {
return context.getGeneratorConfig().getObjectMapper().readTree(rawSchema);
} catch (Exception ex) {
return null;
}
})
.map(CustomPropertyDefinition::new)
.orElse(null);
// if you don't rely on specific field/method functionality,
// you can reuse the same provider for both of them
configBuilder.forFields().withCustomDefinitionProvider(provider);
configBuilder.forMethods().withCustomDefinitionProvider(provider);
When not even the Custom Type Definitions are flexible enough for you and you need to consider the specific field/method context in which a type is being encountered, there is one last path you can take:
SchemaGeneratorConfigBuilder.forFields().withCustomDefinitionProvider()
SchemaGeneratorConfigBuilder.forMethods().withCustomDefinitionProvider()
Order of Object Properties
// sort by custom @JsonPropertyIndex(3) annotation
configBuilder.forTypesInGeneral()
.withPropertySorter(Comparator.comparing(member -> Optional.ofNullable(member.getAnnotation(JsonPropertyIndex.class))
.map(JsonPropertyIndex::value)
.orElse(0)));
// preserve property order in byte code (determined by compiler)
configBuilder.forTypesInGeneral()
.withPropertySorter((first, second) -> 0);
You may want to control the order in which an object's properties are being listed, e.g., when using the JSON schema as basis for an auto-generated form in some user interface.
By default, fields are being included before methods -- with each sublist being sorted alphabetically.
You can define your own Comparator<MemberScope<?, ?>>
, e.g., considering an annotation specifying the desired order or disable the sorting by always returning zero (0
).
With disabled property sorting, your compiler decides the order of the properties in your generated JSON schema. Depending on the compiler, this may correspond to the declaration order in your source file but is not guaranteed.
Jackson Module
The victools:jsonschema-module-jackson provides a number of standard configurations for deriving JSON Schema attributes from jackson
annotations as well as looking up appropriate (annotated) subtypes.
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import com.github.victools.jsonschema.module.jackson.JacksonModule;
import com.github.victools.jsonschema.module.jackson.JacksonOption;
JacksonModule module = new JacksonModule(
JacksonOption.FLATTENED_ENUMS_FROM_JSONVALUE
);
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09)
.with(module);
- Set a field/method's "description" as per
@JsonPropertyDescription
- Set a type's "description" as per
@JsonClassDescription
. - Override a field's/method's property name as per
@JsonProperty
annotations. - Ignore fields/methods that are marked with a
@JsonBackReference
annotation. - Ignore fields (and their associated getter methods) that are deemed to be ignored according to various other
jackson-annotations
(e.g.@JsonIgnore
,@JsonIgnoreType
,@JsonIgnoreProperties
) or are otherwise supposed to be excluded. - Optionally: set a field/method as "required" as per
@JsonProperty
annotations, if theJacksonOption.RESPECT_JSONPROPERTY_REQUIRED
was provided (i.e. this is an "opt-in"). - Optionally: treat enum types as plain strings as per the
@JsonValue
annotated method, if there is one and theJacksonOption.FLATTENED_ENUMS_FROM_JSONVALUE
was provided (i.e. this is an "opt-in"). - Optionally: treat enum types as plain strings, as per each enum constant's
@JsonProperty
annotation, if all values of an enum have such annotations and theJacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY
was provided (i.e. this is an "opt-in"). - Optionally: sort an object's properties according to its
@JsonPropertyOrder
annotation, if theJacksonOption.RESPECT_JSONPROPERTY_ORDER
was provided (i.e. this is an "opt-in"). - Subtype resolution according to
@JsonSubTypes
on a supertype in general or directly on specific fields/methods as an override of the per-type behavior unlessJacksonOption.SKIP_SUBTYPE_LOOKUP
was provided (i.e. this is an "opt-out"). - Apply structural changes for subtypes according to
@JsonTypeInfo
on a supertype in general or directly on specific fields/methods as an override of the per-type behavior unlessJacksonOption.IGNORE_TYPE_INFO_TRANSFORM
was provided (i.e. this is an "opt-out").- Considering
@JsonTypeInfo.include
with valuesAs.PROPERTY
,As.EXISTING_PROPERTY
,As.WRAPPER_ARRAY
,As.WRAPPER_OBJECT
- Considering
@JsonTypeInfo.use
with valuesId.CLASS
,Id.NAME
- Considering
- Consider
@JsonProperty.access
for marking a field/method asreadOnly
orwriteOnly
- Optionally: ignore all methods but those with a
@JsonProperty
annotation, if theJacksonOption.INCLUDE_ONLY_JSONPROPERTY_ANNOTATED_METHODS
was provided (i.e. this is an "opt-in"). - Optionally: respect
@JsonIdentityReference(alwaysAsId=true)
annotation if there is a corresponding@JsonIdentityInfo
annotation on the type and theJacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID
as provided (i.e., this is an "opt-in") - Elevate nested properties to the parent type where members are annotated with
@JsonUnwrapped
. - Support
@JacksonAnnotationsInside
annotated combo annotations
Schema attributes derived from annotations on getter methods are also applied to their associated fields.
To use it, just pass a module instance into SchemaGeneratorConfigBuilder.with(Module)
, optionally providing JacksonOption
values in the module's constructor.
For a description of the available JacksonOption
values and their meaning (apart from their mentionings in above enumeration), please refer to the JacksonOption
class or its JavaDoc.
Jakarta Validation Module
The victools:jsonschema-module-jakarta-validation provides a number of standard configurations for deriving JSON Schema attributes from jakarta.validation.constraints
annotations.
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
JakartaValidationModule module = new JakartaValidationModule(JakartaValidationOption.PREFER_IDN_EMAIL_FORMAT)
.forValidationGroups(YourGroupFlag.class);
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09)
.with(module);
- Determine whether a member is not nullable, base assumption being that all fields and method return values are nullable if not annotated. Based on
@NotNull
/@Null
/@NotEmpty
/@NotBlank
. - Populate list of "required" fields/methods for objects if
JakartaValidationOption.NOT_NULLABLE_FIELD_IS_REQUIRED
/JakartaValidationOption.NOT_NULLABLE_METHOD_IS_REQUIRED
is being provided respectively (i.e. this is an "opt-in"). - Populate "minItems" and "maxItems" for containers (i.e. arrays and collections). Based on
@Size
/@NotEmpty
. - Populate "minProperties" and "maxProperties" for map types. Based on
@Size
/@NotEmpty
. - Populate "minLength" and "maxLength" for strings. Based on
@Size
/@NotEmpty
/@NotBlank
. - Populate "format" for strings. Based on
@Email
, can be "email" or "idn-email" depending on whetherJakartaValidationOption.PREFER_IDN_EMAIL_FORMAT
is being provided. - Populate "pattern" for strings. Based on
@Pattern
/@Email
, ifJakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS
is being provided (i.e. this is an "opt-in"). - Populate "minimum"/"exclusiveMinimum" for numbers. Based on
@Min
/@DecimalMin
/@Positive
/@PositiveOrZero
. - Populate "maximum"/"exclusiveMaximum" for numbers. Based on
@Max
/@DecimalMax
/@Negative
/@NegativeOrZero
. - Populate "enum"/"const" for booleans. Based on
@AssertTrue
/@AssertFalse
.
Schema attributes derived from validation annotations on fields are also applied to their respective getter methods.
Schema attributes derived from validation annotations on getter methods are also applied to their associated fields.
To use it, just pass a module instance into SchemaGeneratorConfigBuilder.with(Module)
, optionally providing JakartaValidationOption
values in the module's constructor and/or specifying validation groups to filter by via .forValidationGroups()
.
module.forValidationGroups(JsonSchemaValidation.class);
The jakarta.validation.constraints
annotations cater for a groups
parameter to be added, to allow different sets of validations to be applied under different circumstances.
Via .forValidationGroups()
you're able to indicate which groups
should be considered during the schema generation.
Without specifying particular groups via .forValidationGroups()
, no filtering will be applied – i.e. all supported jakarta.validation.constraints
annotations will be considered regardless of their respective groups
.
Javax Validation Module
The victools:jsonschema-module-javax-validation provides a number of standard configurations for deriving JSON Schema attributes from javax.validation
annotations.
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import com.github.victools.jsonschema.module.javax.validation.JavaxValidationModule;
import com.github.victools.jsonschema.module.javax.validation.JavaxValidationOption;
JavaxValidationModule module = new JavaxValidationModule(JavaxValidationOption.PREFER_IDN_EMAIL_FORMAT)
.forValidationGroups(YourGroupFlag.class);
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09)
.with(module);
- Determine whether a member is not nullable, base assumption being that all fields and method return values are nullable if not annotated. Based on
@NotNull
/@Null
/@NotEmpty
/@NotBlank
. - Populate list of "required" fields/methods for objects if
JavaxValidationOption.NOT_NULLABLE_FIELD_IS_REQUIRED
/JavaxValidationOption.NOT_NULLABLE_METHOD_IS_REQUIRED
is being provided respectively (i.e. this is an "opt-in"). - Populate "minItems" and "maxItems" for containers (i.e. arrays and collections). Based on
@Size
/@NotEmpty
. - Populate "minLength" and "maxLength" for strings. Based on
@Size
/@NotEmpty
/@NotBlank
. - Populate "format" for strings. Based on
@Email
, can be "email" or "idn-email" depending on whetherJavaxValidationOption.PREFER_IDN_EMAIL_FORMAT
is being provided. - Populate "pattern" for strings. Based on
@Pattern
/@Email
, ifJavaxValidationOption.INCLUDE_PATTERN_EXPRESSIONS
is being provided (i.e. this is an "opt-in"). - Populate "minimum"/"exclusiveMinimum" for numbers. Based on
@Min
/@DecimalMin
/@Positive
/@PositiveOrZero
. - Populate "maximum"/"exclusiveMaximum" for numbers. Based on
@Max
/@DecimalMax
/@Negative
/@NegativeOrZero
.
Schema attributes derived from validation annotations on fields are also applied to their respective getter methods.
Schema attributes derived from validation annotations on getter methods are also applied to their associated fields.
To use it, just pass a module instance into SchemaGeneratorConfigBuilder.with(Module)
, optionally providing JavaxValidationOption
values in the module's constructor and/or specifying validation groups to filter by via .forValidationGroups()
.
module.forValidationGroups(JsonSchemaValidation.class);
The javax.validation
annotations cater for a groups
parameter to be added, to allow different sets of validations to be applied under different circumstances.
Via .forValidationGroups()
you're able to indicate which groups
should be considered during the schema generation.
Without specifying particular groups via .forValidationGroups()
, no filtering will be applied – i.e. all supported javax.validation
annotations will be considered regardless of their respective groups
.
Swagger 1.5 Module
The victools:jsonschema-module-swagger-1.5 provides a number of standard configurations for deriving JSON Schema attributes from swagger
(1.5.x) annotations.
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import com.github.victools.jsonschema.module.swagger15.SwaggerModule;
import com.github.victools.jsonschema.module.swagger15.SwaggerOption;
SwaggerModule module = new SwaggerModule(
SwaggerOption.ENABLE_PROPERTY_NAME_OVERRIDES,
SwaggerOption.IGNORING_HIDDEN_PROPERTIES
);
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09)
.with(module);
- Set a field/method's "description" as per
@ApiModelProperty(value = ...)
- Set a type's "title" as per
@ApiModel(value = ...)
unlessSwaggerOption.NO_APIMODEL_TITLE
was provided (i.e. this is an "opt-out") - Set a type's "description" as per
@ApiModel(description = ...)
unlessSwaggerOption.NO_APIMODEL_DESCRIPTION
was provided (i.e. this is an "opt-out") - Ignore a field/method if
@ApiModelProperty(hidden = true)
andSwaggerOption.IGNORING_HIDDEN_PROPERTIES
was provided (i.e. this is an "opt-in") - Override a field's property name as per
@ApiModelProperty(name = ...)
ifSwaggerOption.ENABLE_PROPERTY_NAME_OVERRIDES
was provided (i.e. this is an "opt-in") - Indicate a number's (field/method) "minimum" (inclusive) according to
@ApiModelProperty(allowableValues = "range[...")
- Indicate a number's (field/method) "exclusiveMinimum" according to
@ApiModelProperty(allowableValues = "range(...")
- Indicate a number's (field/method) "maximum" (inclusive) according to
@ApiModelProperty(allowableValues = "range...]")
- Indicate a number's (field/method) "exclusiveMaximum" according to
@ApiModelProperty(allowableValues = "range...)")
- Indicate a field/method's "const"/"enum" as per
@ApiModelProperty(allowableValues = ...)
if it is not a numeric range declaration
Schema attributes derived from @ApiModelProperty
on fields are also applied to their respective getter methods.
Schema attributes derived from @ApiModelProperty
on getter methods are also applied to their associated fields.
To use it, just pass a module instance into SchemaGeneratorConfigBuilder.with(Module)
, optionally providing SwaggerOption
values in the module's constructor.
Swagger 2 Module
The victools:jsonschema-module-swagger-2 provides a number of standard configurations for deriving JSON Schema attributes from OpenAPI/swagger
(2.x) @Schema
annotations.
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
Swagger2Module module = new Swagger2Module();
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09)
.with(module);
- From
@Schema(description = …)
on types in general, derive"description"
. - From
@Schema(title = …)
on types in general, derive"title"
. - From
@Schema(ref = …)
on types in general, replace subschema with"$ref"
to external/separate schema (except for the main type being targeted). - From
@Schema(subTypes = …)
on types in general, derive"anyOf"
alternatives. - From
@Schema(anyOf = …)
on types in general (as alternative tosubTypes
), derive"anyOf"
alternatives. - From
@Schema(name = …)
on types in general, derive the keys/names in"definitions"
/"$defs"
. - From
@Schema(description = …)
on fields/methods, derive"description"
. - From
@Schema(title = …)
on fields/methods, derive"title"
. - From
@Schema(implementation = …)
on fields/methods, override represented type. - From
@Schema(hidden = true)
on fields/methods, skip certain properties. - From
@Schema(name = …)
on fields/methods, override property names. - From
@Schema(ref = …)
on fields/methods, replace subschema with"$ref"
to external/separate schema. - From
@Schema(allOf = …)
on fields/methods, include"allOf"
parts. - From
@Schema(anyOf = …)
on fields/methods, include"anyOf"
parts. - From
@Schema(oneOf = …)
on fields/methods, include"oneOf"
parts. - From
@Schema(not = …)
on fields/methods, include the indicated"not"
subschema. - From
@Schema(requiredMode = REQUIRED)
or@Schema(required = true)
on fields/methods, mark property as"required"
in the schema containing the property. - From
@Schema(requiredProperties = …)
on fields/methods, derive its"required"
properties. - From
@Schema(minProperties = …)
on fields/methods, derive its"minProperties"
. - From
@Schema(maxProperties = …)
on fields/methods, derive its"maxProperties"
. - From
@Schema(nullable = true)
on fields/methods, includenull
in its"type"
. - From
@Schema(allowableValues = …)
on fields/methods, derive its"const"
/"enum"
. - From
@Schema(defaultValue = …)
on fields/methods, derive its"default"
. - From
@Schema(accessMode = AccessMode.READ_ONLY)
on fields/methods, to mark them as"readOnly"
. - From
@Schema(accessMode = AccessMode.WRITE_ONLY)
on fields/methods, to mark them as"writeOnly"
. - From
@Schema(minLength = …)
on fields/methods, derive its"minLength"
. - From
@Schema(maxLength = …)
on fields/methods, derive its"maxLength"
. - From
@Schema(format = …)
on fields/methods, derive its"format"
. - From
@Schema(pattern = …)
on fields/methods, derive its"pattern"
. - From
@Schema(multipleOf = …)
on fields/methods, derive its"multipleOf"
. - From
@Schema(minimum = …, exclusiveMinimum = …)
on fields/methods, derive its"minimum"
/"exclusiveMinimum"
. - From
@Schema(maximum = …, exclusiveMaximum = …)
on fields/methods, derive its"maximum"
/"exclusiveMaximum"
. - From
@ArraySchema(minItems = …)
on fields/methods, derive its"minItems"
. - From
@ArraySchema(maxItems = …)
on fields/methods, derive its"maxItems"
. - From
@ArraySchema(uniqueItems = …)
on fields/methods, derive its"uniqueItems"
.
Schema attributes derived from @Schema
/@ArraySchema
on fields are also applied to their respective getter methods.
Schema attributes derived from @Schema
/@ArraySchema
on getter methods are also applied to their associated fields.
To use it, just pass a module instance into SchemaGeneratorConfigBuilder.with(Module)
.
Maven Plugin
The victools:jsonschema-maven-plugin allows you to incorporate the generation of JSON Schemas from your code into your build process.
There are a number of basic configuration options as well as the possibility to define any kind of configurations through a Module
of your own.
Target Types to generate Schemas for
<plugin>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<classNames>com/myOrg/myApp/My*</classNames>
<packageNames>com/myOrg/myApp/package?</packageNames>
<excludeClassNames>com/myOrg/myApp/**Hidden*</excludeClassNames>
<annotations>
<annotation>com.myOrg.MySchemaAnnotation</annotation>
</annotations>
<skipAbstractTypes>true</skipAbstractTypes>
<skipInterfaces>true</skipInterfaces>
<classpath>PROJECT_ONLY</classpath>
<failIfNoClassesMatch>false</failIfNoClassesMatch>
</configuration>
</plugin>
The designated types can be mentioned separately (with dots as package separator) or in the form of glob patterns (with /
as package separator) in <classNames>
and/or included as part of their packages through <packageNames>
. Through <excludeClassNames>
you can further narrow down the type selection.
When specifying expected <annotations>
, at least one of them need to be present on a given type to be considered – in addition to matching the aforementioned criteria, if those are present.
The considered <classpath>
may be further specified as one of four values:
- PROJECT_ONLY
: only source files of the current project
- WITH_COMPILE_DEPENDENCIES
: PROJECT_ONLY
and compile dependencies
- WITH_RUNTIME_DEPENDENCIES
: PROJECT_ONLY
and runtime dependencies (default, if unspecified)
- WITH_ALL_DEPENDENCIES
: all of the above
- WITH_ALL_DEPENDENCIES_AND_TESTS
: all of the above, with the addition of the current project's test files
Note that this requires a different <phase>
(e.g., test-compile
) being specified on the <execution>
.
By default, the plugin aborts if the glob pattern does not match any class. If this is not desired, the <failIfNoClassesMatch>
property can be set to false
.
Basic Configuration Options
There are some additional parameters available in the plugin <configuration>
:
# | Tag | Default | Description |
---|---|---|---|
1 | <schemaFilePath> |
src/main/resources |
Directory to generate all schemas in |
2 | <schemaFileName> |
{0}-schema.json |
Relative path from the <schemaFilePath> including the file name pattern. Two placeholders are supported: {0} will be replaced with the respective simple class name (e.g. TypeA ) {1} will be replaced with the respective package path (e.g. com/myOrg/myApp ) in case you want to preserve the original package structure |
3 | <schemaVersion> |
DRAFT_7 |
JSON Schema version to apply (DRAFT_6 , DRAFT_7 , DRAFT_2019_09 or DRAFT_2020_12 ) |
Configuring generated file names and locations
<configuration>
<classNames>com.myOrg.myApp.MyClass</classNames>
<schemaFilePath>src/main/resources/schemas</schemaFilePath>
</configuration>
The location where the files will be generated can be specified with the <schemaFilePath>
element.
The default path is src/main/resources
<configuration>
<classNames>com.myOrg.myApp.MyClass</classNames>
<schemaFileName>{0}.schema</schemaFileName>
</configuration>
The name of the generated schema files can be configured with the <schemaFileName>
element.
This is a substitution pattern that is used for all generated files. It is following the MessageFormat
syntax,
where the following variables can be used:
- {0}
: This is the name of the class
- {1}
: This is the package path of the class
For example, the given configuration will create a MyClass.schema
file.
<configuration>
<packageNames>com.myOrg.myApp.utils</packageNames>
<schemaFileName>{1}/{0}.schema</schemaFileName>
</configuration>
To store the generated schema files in the same directory structure as the originating classes, the following can be used <schemaFileName>{1}/{0}-schema.json</schemaFileName>
.
The default <schemaFileName>
is {0}-schema.json
.
Additionally, you can omit the generation for abstract classes and/or interfaces by setting the respective <skipAbstractTypes>
or <skipInterfaces>
flags to true
(by default, they are false
).
xml
<configuration>
<packageNames>com/myOrg/myApp/package/**</packageNames>
<skipAbstractTypes>true</skipAbstractTypes>
<skipInterfaces>true</skipInterfaces>
</configuration>
Selecting Options
<options>
<preset>FULL_DOCUMENTATION</preset>
<enabled>
<option>DEFINITIONS_FOR_ALL_OBJECTS</option>
<option>FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT</option>
</enabled>
<disabled>SCHEMA_VERSION_INDICATOR</disabled>
</options>
The standard generator Option
s can be included via the <options>
tag.
Further configurations through Modules
<modules>
<module>
<name>Jackson</name>
<options>
<option>FLATTENED_ENUMS_FROM_JSONVALUE</option>
</options>
</module>
</modules>
Through the <modules>
tag you can include the standard modules – potentially with their <options>
if there are any.
<modules>
<module>
<className>com.myOrg.myApp.CustomModule</className>
</module>
</modules>
You can also group any kind of configurations into a Module of your own and include it via its full class name.
Make sure your custom module is on the classpath (considering the project itself as well as all compile and runtime dependencies) and has a default constructor.
It is not possible to configure options for custom modules.
Altering the format of generated schema files
public class MavenPluginYamlModule implements Module {
@Override
public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
// Maven plugin should produce YAML files instead of JSON
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
// set additional serialization options
mapper.getSerializationConfig()
.with(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS);
mapper.setNodeFactory(JsonNodeFactory.withExactBigDecimals(true));
builder.withObjectMapper(mapper);
}
}
One possibility within such a custom Module (as mentioned above) is to configure the format of the generated schema files.
The file contents are being produced by the schema generator's associated ObjectMapper
.
That default ObjectMapper
can be replaced, e.g., to opt-out of the default pretty-printing or changing the file format to YAML.
The given example requires the inclusion of the extra com.fasterxml.jackson.dataformat:jackson-dataformat-yaml
dependency.
Loading custom Module from test classes
<executions>
<execution>
<phase>test-compile</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
When you're using a custom Module (as mentioned above) for additional configuration options, but don't want to include it among your application code,
you can either package it as separate artifact and include that as dependency of the plugin (not going into further detail here)
or the custom Module class can be included in your test packages.
When you do the latter, the Maven plugin will by default not be able to load that class, since it won't be compiled yet in the Maven phase during which the schema generation is being executed.
The Maven compile
phase is when the schema generation gets triggered by default.
If you want the test classes (including the custom Module) to be available, a later phase (most likely: test-compile
) needs to be specified.
FAQ
Is there a Gradle Plugin?
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath group: 'com.github.victools', name: 'jsonschema-generator', version: '4.16.0'
}
}
plugins {
id 'java-library'
}
task generate {
doLast {
def configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON);
// apply your configurations here
def generator = new SchemaGenerator(configBuilder.build());
// target the class for which to generate a schema
def jsonSchema = generator.generateSchema(SchemaVersion.class);
// handle generated schema, e.g. write it to the console or a file
def jsonSchemaAsString = jsonSchema.toPrettyString();
println jsonSchemaAsString
new File(projectDir, "schema.json").text = jsonSchemaAsString
}
}
There currently is no dedicated Gradle Plugin as such, but Gradle is flexible enough to allow you to use a java library straight from within the build.gradle
file.
Checkout https://github.com/victools/jsonschema-gradle-example for the complete example.
What about enums?
If you have a custom serialization logic for converting enum values to strings, you can re-use it in order to generate the correct list of allowed values:
ObjectMapper objectMapper = new ObjectMapper();
// make use of your enum handling e.g. through your own serializer
// objectMapper.registerModule(new YourCustomEnumSerializerModule());
configBuilder.with(new EnumModule(possibleEnumValue -> {
try {
String valueInQuotes = objectMapper.writeValueAsString(possibleEnumValue);
return valueInQuotes.substring(1, valueInQuotes.length() - 1);
} catch (JsonProcessingException ex) {
throw new IllegalStateException(ex);
}
}));
Enums are a special construct for which there are multiple options:
Option.FLATTENED_ENUMS
(which is part of theOptionPreset.PLAIN_JSON
)- This defines an enum as
{ "type": "string", "enum": ["VALUE1", "VALUE2"] }
with thename()
method being called on each possible enum value. - If there is only one enum value, it will be set as
"const"
instead. - Such an enum representation will always be in-lined and not moved into the
"definitions"
/"$defs"
.
- This defines an enum as
Option.SIMPLIFIED_ENUMS
(which is part of theOptionPreset.JAVA_OBJECT
andOptionPreset.FULL_DOCUMENTATION
)- This treats enums like any other class but hiding some methods and listing the possible enum values as
"enum"
/"const"
on thename()
method.
- This treats enums like any other class but hiding some methods and listing the possible enum values as
- Using neither of the two
Option
s above will let them be handled like any other class (unless there are further configurations taking care of enums). - The
JacksonModule
comes with two more alternatives:JacksonOption.FLATTENED_ENUMS_FROM_JSONVALUE
, behaving likeOption.FLATTENED_ENUMS
but looking-up the respective values via the@JsonValue
annotated method.JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY
, behaving likeOption.FLATTENED_ENUMS
but looking-up the respective values via the@JsonProperty
annotation on each enum value/constant.
- Write your own custom definition provider or re-use the
EnumModule
class as in the shown example.
How to always inline enums?
class InlineAllEnumsDefinitionProvider implements CustomDefinitionProviderV2 {
@Override
public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
if (javaType.isInstanceOf(Enum.class)) {
ObjectNode standardDefinition = context.createStandardDefinition(javaType, this);
return new CustomDefinition(standardDefinition,
CustomDefinition.DefinitionType.INLINE,
CustomDefinition.AttributeInclusion.YES);
}
return null;
}
}
configBuilder.forTypesInGeneral()
.withCustomDefinitionProvider(new InlineAllEnumsDefinitionProvider())
If you want to generally avoid that enums are being referenced via the $defs
/definitions
(even with active Option.DEFINITIONS_FOR_ALL_OBJECTS
), a construct like the InlineAllEnumsDefinitionProvider
on the right can be used.
As usual, such a Custom Type Definition can be added via configBuilder.forTypesInGeneral().withCustomDefinitionProvider()
accordingly.
Where can I find some more configuration examples?
Internally, a number of the standard Option
s are realized via Individual Configurations and/or Advanced Configurations – grouped into Module
s.
These make for excellent examples to get you started into your own setup, if the existing Option
s do not cover your specific requirements.
How to represent a Map<K, V>
in a generated schema?
configBuilder.forTypesInGeneral()
.withPatternPropertiesResolver((scope) -> {
if (scope.getType().isInstanceOf(Map.class)) {
// within a Map<Key, Value> allow additional properties of the Value type, with purely alphabetic keys
Map<String, Type> patternProperties = new HashMap<>();
// theoretically, you could derive an appropriate schema from the key type, accessible via the same getTypeParameterFor() method
// if no type parameters are defined, this will result in `{}` to be set as value schema and thereby allowing any values for matching keys
patternProperties.put("^[a-zA-Z]+$", scope.getTypeParameterFor(Map.class, 1));
return patternProperties;
}
return null;
});
By default, a Map
will be treated like any other type – i.e. most likely a simple { "type": "object" }
without many further details if you use the OptionPreset.PLAIN_JSON
or otherwise ignore methods.
The following are the two most common approaches:
- Indicate the value type
V
as the expected type for any"additionalProperties"
by including theOption.MAP_VALUES_AS_ADDITIONAL_PROPERTIES
.
You may also want to consider including theOption.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT
to forbid"additionalProperties"
everywhere else. - If you have a clear idea of how the key type
K
will be serialized, you could also describe valid keys via"patternProperties"
instead – as per the example on the right.
Refer to https://json-schema.org/understanding-json-schema/reference/regular_expressions.html for a description of how to build valid patterns.
How to populate default
values?
Example 1
configBuilder.forFields().withDefaultResolver(field -> {
JsonProperty annotation = field.getAnnotationConsideringFieldAndGetter(JsonProperty.class);
return annotation == null || annotation.defaultValue().isEmpty() ? null : annotation.defaultValue();
});
Example 2
ConcurrentMap<Class<?>, Object> instanceCache = new ConcurrentHashMap<>();
configBuilder.forFields().withDefaultResolver(field -> {
Class<?> declaringClass = field.getDeclaringType().getErasedType();
if (!field.isFakeContainerItemScope()
&& declaringClass.getName().startsWith("your.package")) {
MethodScope getter = field.findGetter();
if (getter != null) {
try {
Object instance = instanceCache.computeIfAbsent(declaringClass, declaringClass::newInstance);
Object defaultValue = getter.getRawMember().invoke(instance);
return defaultValue;
} catch (Exception ex) {
// most likely missing a no-args constructor
}
}
}
return null;
});
The short answer is: via the withDefaultResolver()
– one of the Individual Configurations.
The exact details depend on how the default
value can be determined.
- If the
default
value is explicitly stated via some kind of annotation, it might be as simple as "Example 1" on the right. - If the
default
value is only set in code, and you cannot or don't want to maintain that piece of information twice this can get a bit more advanced. Here assuming your own classes all have a default no-args constructor and conventional getters as in "Example 2" on the right.
How to reference a separate schema/file?
configBuilder.forTypesInGeneral()
.withCustomDefinitionProvider((javaType, context) -> {
if (javaType.getErasedType() != MyExternalType.class) {
// other types should be treated normally
return null;
}
// define your custom reference value
String refValue = "./" + javaType.getErasedType().getSimpleName();
// produce the sub-schema that only contains your custom reference
ObjectNode customNode = context.getGeneratorConfig().createObjectNode()
.put(context.getKeyword(SchemaKeyword.TAG_REF), refValue);
return new CustomDefinition(customNode,
// avoid the creation of a reference to your custom reference schema
CustomDefinition.DefinitionType.INLINE,
// still allow for collected schema attributes to be added
CustomDefinition.AttributeInclusion.YES);
});
By using withCustomDefinitionProvider()
– one of the advanced configurations – you can fully control the contents of a type's sub-schema.
Simply create a node that only contains your custom/external reference instead of the actual schema.
It is recommended to mark the custom definition as "to be inlined", in order to avoid an extra entry in the "definitions"
/"$defs"
.
Motivation
This started out as (and still is) a personal project. After writing Java code professionally for over a decade, I am more and more consumed with rather theoretical work in my role as a business analyst and product owner – i.e. writing specifications instead of code. However, my past still causes me to research available libraries and components when coming up with ideas for new features, based on which I can then (at least try to) write "realistic" specifications, i.e. features that can be feasibly implemented without requiring our developers to create something from scratch (who'd want to spend their valuable time and resources for that?). Open Source is awesome! Unfortunately, such research does not always yield satisfying results.
The background for this particular requirement was very close to my heart as it affected me personally quite often. I'm working at Torque IT Solutions – a small company that puts a lot of emphasis on providing software products instead of building custom applications for each customer. That means, such a product needs to be generic enough to be a fit for every customer. Amongst other things, we do this through heaps of configuration options. In order to achieve the desired flexibility, these configurations are sometimes very technical. E.g. allowing our customers (i.e. users) to define JavaScript expressions based on our own Java DOM. In reality this means quite often: customers can configure it themselves but will still ask us/me to provide those expressions. However, that poses the challenge of documenting our DOM in a way that it can be used without having access to the code itself or its JavaDoc. I couldn't find a nice way of doing the above without sentencing myself to constantly maintain a huge amount of documentation.
It was development time again! I only needed to find some existing standard for documenting data structures (there would surely be a way of automatically generating it from code then) and somehow visualize it, to allow non-developers to use it. I quickly decided on the JSON Schema specification (although still in Draft version 6 then) and started working on my react-jsonschema-inspector component. While I had already spent considerable amounts of my spare time on this frontend component (my first ReactJS component – I really missed the strong Java typing!), I realized that the existing JSON Schema generation libraries typically expected some specific annotations throughout the code base for the sole purpose of generating a JSON Schema. I surely wasn't expecting our developers to go through hundreds of classes and specifying schema parts (by this time Draft version 7) throughout the codebase. And none of the existing generation libraries seemed to allow for methods to be documented (understandable, if you only aim at documenting a JSON structure, but not good enough for my purposes after all).
Once more: development time! A new JSON Schema generation library needed to be created, as Open Source of course! At
least I was back in my familiar Java world. The whole topic of introspecting java types was a fun challenge that ended
in me adopting the use of the awesome com.fasterxml/classmate
library – written by one of the maintainers of the
Jackson library, i.e. one who had worked with this kind of thing for a number of years already.
Then it was only about abstracting the actual schema generation from its configuration while still allowing almost all
aspects of the process to be customized to not force someone else to create yet another generation library just because
mine was too opinionated to be reusable (especially since Draft 2019-09 had just been published).
In our existing codebase, we already had annotations for various purposes: Jackson annotations to facilitate the (de)serialization for our Rest API, Swagger annotations to document that Rest API, javax.validation annotations for realizing automatic validations of incoming data and during persistence. I very much liked the modular configuration approach in Jackson so I ended up employing the same principle to wrap a few standard configurations for easier re-use. Feedback from some early adopters led to those few standard configurations to be further extended – especially the subtype resolution/polymorphism seemed to have been an important point also in the creation of other libraries, e.g. for the mbknor-jackson-jsonSchema.