diff --git a/slate-docs/Gemfile b/slate-docs/Gemfile index b2cc3659..554f508e 100644 --- a/slate-docs/Gemfile +++ b/slate-docs/Gemfile @@ -1,12 +1,13 @@ -ruby '>=2.3.1' +ruby '>= 2.6' source 'https://rubygems.org' # Middleman -gem 'middleman', '~>4.3' +gem 'middleman', '~> 4.4' gem 'middleman-syntax', '~> 3.2' -gem 'middleman-autoprefixer', '~> 2.7' +gem 'middleman-autoprefixer', '~> 3.0' gem 'middleman-sprockets', '~> 4.1' -gem 'rouge', '~> 3.20' -gem 'redcarpet', '~> 3.5.1' +gem 'rouge', '~> 3.21' +gem 'redcarpet', '~> 3.6.0' gem 'nokogiri', '~> 1.14.3' gem 'sass' +gem 'webrick' diff --git a/slate-docs/Gemfile.lock b/slate-docs/Gemfile.lock index 1da90c2b..dc02f352 100644 --- a/slate-docs/Gemfile.lock +++ b/slate-docs/Gemfile.lock @@ -1,59 +1,59 @@ GEM remote: https://rubygems.org/ specs: - activesupport (5.2.4.3) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) - autoprefixer-rails (9.5.1.1) - execjs - backports (3.18.1) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + autoprefixer-rails (10.2.5.0) + execjs (< 2.8.0) + backports (3.23.0) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.7) - contracts (0.13.0) - dotenv (2.7.6) + concurrent-ruby (1.2.2) + contracts (0.16.1) + dotenv (2.8.1) erubis (2.7.0) execjs (2.7.0) - fast_blank (1.0.0) - fastimage (2.2.0) - ffi (1.13.1) - haml (5.1.2) + fast_blank (1.0.1) + fastimage (2.2.6) + ffi (1.15.5) + haml (5.2.2) temple (>= 0.8.0) tilt hamster (3.0.0) concurrent-ruby (~> 1.0) hashie (3.6.0) - i18n (0.9.5) + i18n (1.6.0) concurrent-ruby (~> 1.0) - kramdown (2.3.1) + kramdown (2.4.0) rexml - listen (3.0.8) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) memoist (0.16.2) - middleman (4.3.8) + middleman (4.4.3) coffee-script (~> 2.2) - haml (>= 4.0.5) + haml (>= 4.0.5, < 6.0) kramdown (>= 2.3.0) - middleman-cli (= 4.3.8) - middleman-core (= 4.3.8) - middleman-autoprefixer (2.10.1) - autoprefixer-rails (~> 9.1) - middleman-core (>= 3.3.3) - middleman-cli (4.3.8) + middleman-cli (= 4.4.3) + middleman-core (= 4.4.3) + middleman-autoprefixer (3.0.0) + autoprefixer-rails (~> 10.0) + middleman-core (>= 4.0.0) + middleman-cli (4.4.3) thor (>= 0.17.0, < 2.0) - middleman-core (4.3.8) - activesupport (>= 4.2, < 6.0) - addressable (~> 2.3) + middleman-core (4.4.3) + activesupport (>= 6.1, < 7.1) + addressable (~> 2.4) backports (~> 3.6) - bundler - contracts (~> 0.13.0) + bundler (~> 2.0) + contracts (~> 0.13) dotenv erubis execjs (~> 2.0) @@ -61,43 +61,45 @@ GEM fastimage (~> 2.0) hamster (~> 3.0) hashie (~> 3.4) - i18n (~> 0.9.0) - listen (~> 3.0.0) + i18n (~> 1.6.0) + listen (~> 3.0) memoist (~> 0.14) - padrino-helpers (~> 0.13.0) + padrino-helpers (~> 0.15.0) parallel rack (>= 1.4.5, < 3) sassc (~> 2.0) servolux tilt (~> 2.0.9) + toml uglifier (~> 3.0) + webrick middleman-sprockets (4.1.1) middleman-core (~> 4.0) sprockets (>= 3.0) - middleman-syntax (3.2.0) + middleman-syntax (3.3.0) middleman-core (>= 3.2) rouge (~> 3.2) - mini_portile2 (2.8.1) - minitest (5.14.1) - nokogiri (1.14.3) + mini_portile2 (2.8.5) + minitest (5.20.0) + nokogiri (1.14.5) mini_portile2 (~> 2.8.0) racc (~> 1.4) - padrino-helpers (0.13.3.4) - i18n (~> 0.6, >= 0.6.7) - padrino-support (= 0.13.3.4) + padrino-helpers (0.15.2) + i18n (>= 0.6.7, < 2) + padrino-support (= 0.15.2) tilt (>= 1.4.1, < 3) - padrino-support (0.13.3.4) - activesupport (>= 3.1) - parallel (1.19.2) - public_suffix (4.0.6) - racc (1.6.2) - rack (2.2.6.4) - rb-fsevent (0.10.4) + padrino-support (0.15.2) + parallel (1.22.1) + parslet (2.0.0) + public_suffix (5.0.1) + racc (1.7.3) + rack (2.2.6.2) + rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - redcarpet (3.5.1) + redcarpet (3.6.0) rexml (3.2.5) - rouge (3.20.0) + rouge (3.30.0) sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -109,30 +111,33 @@ GEM sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - temple (0.8.2) - thor (1.0.1) - thread_safe (0.3.6) - tilt (2.0.10) - tzinfo (1.2.10) - thread_safe (~> 0.1) + temple (0.10.0) + thor (1.2.1) + tilt (2.0.11) + toml (0.3.0) + parslet (>= 1.8.0, < 3.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) uglifier (3.2.0) execjs (>= 0.3.0, < 3) + webrick (1.8.1) PLATFORMS ruby DEPENDENCIES - middleman (~> 4.3) - middleman-autoprefixer (~> 2.7) + middleman (~> 4.4) + middleman-autoprefixer (~> 3.0) middleman-sprockets (~> 4.1) middleman-syntax (~> 3.2) nokogiri (~> 1.14.3) - redcarpet (~> 3.5.1) - rouge (~> 3.20) + redcarpet (~> 3.6.0) + rouge (~> 3.21) sass + webrick RUBY VERSION - ruby 2.3.3p222 + ruby 2.7.2p137 BUNDLED WITH - 2.1.4 + 2.2.22 diff --git a/slate-docs/source/includes/_main-generator-advanced.md b/slate-docs/source/includes/_main-generator-advanced.md new file mode 100644 index 00000000..ecde5512 --- /dev/null +++ b/slate-docs/source/includes/_main-generator-advanced.md @@ -0,0 +1,258 @@ +# 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 +```java +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](#generator-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 +```java +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](#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 a `String` or a `Number` but there is no common supertype but `Object` that can be declared: + +```java +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: + +```java +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: + +```json +{ + "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 the `Cat` and `Dog` implementations: + +```java +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](#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): + +```java +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()`: + +```java +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()`: + +```java +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()`: + +```java +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()`: + +```java +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); +``` + + + +1. `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. +2. `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. +3. `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. +4. `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 produce `ResolvedType` instances which are expected by various other methods. + + + +## Custom Property Definitions +```java +// 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](#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 +```java +// sort by custom @JsonPropertyIndex(3) annotation +configBuilder.forTypesInGeneral() + .withPropertySorter(Comparator.comparing(member -> Optional.ofNullable(member.getAnnotation(JsonPropertyIndex.class)) + .map(JsonPropertyIndex::value) + .orElse(0))); +``` +```java +// 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>`, 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. diff --git a/slate-docs/source/includes/_main-generator-individual.md b/slate-docs/source/includes/_main-generator-individual.md new file mode 100644 index 00000000..11093a81 --- /dev/null +++ b/slate-docs/source/includes/_main-generator-individual.md @@ -0,0 +1,554 @@ +# Generator – Individual Configurations +> E.g. for the given configuration: + +```java +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: + +```java +class MyClass { + public List texts; +} +``` + +> The following schema will be generated: + +```json +{ + "type": "object", + "properties": { + "texts": { + "type": "array", + "title": "texts = (real) List", + "description": "original type = List", + "items": { + "type": "string", + "title": "texts = (fake) String", + "description": "original type = List" + } + } + } +} +``` + +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](https://github.com/victools/jsonschema-generator/tree/master/jsonschema-generator#supported-json-schema-attributes) 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 +```java +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 +```java +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 +```java +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"` +```java +configBuilder.forTypesInGeneral() + .withDefinitionNamingStrategy(new DefaultSchemaDefinitionNamingStrategy() { + @Override + public String getDefinitionNameForKey(DefinitionKey key, SchemaGenerationContext context) { + return super.getDefinitionNameForKey(key, generationContext).toLowerCase(); + } + @Override + public void adjustDuplicateNames(Map 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` and `your.pkg.DateTime` would be represented by `DateTime-1` and `DateTime-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 the `null`able one getting a `"-nullable"` suffix to its definition name. + + + +## Names of fields/methods in an object's `properties` +```java +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 +```java +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` +```java +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 whether `Option.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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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. + +```java +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 registered `AdditionalPropertiesResolver` will be checked. If all return `null`, 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 to `false`. +* If any other type is being returned (e.g. other `Class` or a `ResolvedType`) 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. + +```java +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 registered `AdditionalPropertiesResolver` will be checked. If all return `null`, 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 to `false`. +* 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` 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. + +```java +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` when that is the declared type of a field/method. + +## `"minLength"` Keyword +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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 +```java +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. diff --git a/slate-docs/source/includes/_main-generator-modules.md b/slate-docs/source/includes/_main-generator-modules.md new file mode 100644 index 00000000..1f8f2a67 --- /dev/null +++ b/slate-docs/source/includes/_main-generator-modules.md @@ -0,0 +1,10 @@ +# 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](#generator-individual-configurations) or even [advanced configurations](#generator-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. + + diff --git a/slate-docs/source/includes/_main-generator-options.md b/slate-docs/source/includes/_main-generator-options.md new file mode 100644 index 00000000..c2111135 --- /dev/null +++ b/slate-docs/source/includes/_main-generator-options.md @@ -0,0 +1,370 @@ +# 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. + +```java +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 includedBehavior if excluded
1Option.SCHEMA_VERSION_INDICATOR
Setting appropriate $schema attribute on main schema being generated.No $schema attribute is being added.
2Option.ADDITIONAL_FIXED_TYPES
+
    +
  • String/Character/char/CharSequence are treated as { "type": "string" } schema
  • +
  • Boolean/boolean are treated as { "type": "boolean" } schema
  • +
  • Integer/int/Long/long/Short/short/Byte/byte are treated as { "type": "integer" } schema
  • +
  • Double/double/Float/float are treated as { "type": "number" } schema
  • +
  • BigInteger as { "type": "integer" } schema
  • +
  • BigDecimal/Number as { "type": "number" } schema
  • +
  • LocalDate/LocalDateTime/LocalTime/ZonedDateTime/OffsetDateTime/OffsetTime/Instant/Period/ZoneId/Date/Calendar/UUID as { "type": "string" } schema
  • +
+
+
    +
  • String/Character/char/CharSequence are treated as { "type": "string" } schema
  • +
  • Boolean/boolean are treated as { "type": "boolean" } schema
  • +
  • Integer/int/Long/long/Short/short/Byte/byte are treated as { "type": "integer" } schema
  • +
  • Double/double/Float/float are treated as { "type": "number" } schema
  • +
+
3Option.EXTRA_OPEN_API_FORMAT_VALUES
Include extra "format" values (e.g. "int32", "int64", "date", "date-time", "uuid") for fixed types (primitive/basic types, plus some of the Option.ADDITIONAL_FIXED_TYPES if they are enabled as well).no automatic "format" values are being included.
4Option.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.-
#Behavior if includedBehavior if excluded
5Option.FLATTENED_ENUMS
Treating encountered enum types as { "type": "string" } schema with the names of the enum constants being listed as its enum values.-
6Option.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.-
7Option.SIMPLIFIED_OPTIONALS
Treating encountered Optional instances as objects, but including only the get(), orElse() and isPresent() methods.-
8Option.FLATTENED_OPTIONALS
Replacing encountered Optional instances as null-able forms of their generic parameter type.-
#Behavior if includedBehavior if excluded
9Option.FLATTENED_SUPPLIERS
Replacing encountered Supplier instances with their generic parameter type.-
10Option.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.
11Option.PUBLIC_STATIC_FIELDS
Include public static fields in an object's properties.No public static fields are included in an object's properties.
12Option.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.
#Behavior if includedBehavior if excluded
13Option.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.
14Option.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.
15Option.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.
16Option.TRANSIENT_FIELDS
Include transient fields in an object's properties if they would otherwise be included according to the Options above.No transient fields are included in an object's properties even if they would otherwise be included according to the Options above.
#Behavior if includedBehavior if excluded
17Option.STATIC_METHODS
Include public static methods in an object's propertiesNo static methods are included in an object's properties even if they would be included according to the Option.VOID_METHODS below.
18Option.VOID_METHODS
Include public void methods in an object's propertiesNo void methods are included in an object's properties even if they would be included according to the Option.STATIC_METHODS above.
19Option.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.
20Option.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.
#Behavior if includedBehavior if excluded
21Option.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.
22Option.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.
23Option.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.
24Option.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.
#Behavior if includedBehavior if excluded
25Option.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.
26Option.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.
27Option.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.
28Option.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.
#Behavior if includedBehavior if excluded
29Option.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".
30Option.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.
31Option.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.
32Option.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).
#Behavior if includedBehavior if excluded
33Option.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.
34Option.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.
+ +Below, you can find the lists of Options included/excluded in the respective standard OptionPresets: + +* "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 | `EXTRA_OPEN_API_FORMAT_VALUES` | ⬜️ | ⬜️ | ⬜️ | +| 4 | `SIMPLIFIED_ENUMS` | ✅ | ✅ | ⬜️ | +| 5 | `FLATTENED_ENUMS` | ⬜️ | ⬜️ | ✅ | +| 6 | `FLATTENED_ENUMS_FROM_TOSTRING` | ⬜️ | ⬜️ | ⬜️ | +| 7 | `SIMPLIFIED_OPTIONALS` | ✅ | ✅ | ⬜️ | +| 8 | `FLATTENED_OPTIONALS` | ⬜️ | ⬜️ | ✅ | +| 8 | `FLATTENED_SUPPLIERS` | ⬜️ | ⬜️ | ✅ | +| 10 | `VALUES_FROM_CONSTANT_FIELDS` | ✅ | ✅ | ✅ | +| 11 | `PUBLIC_STATIC_FIELDS` | ✅ | ✅ | ⬜️ | +| 12 | `PUBLIC_NONSTATIC_FIELDS` | ✅ | ✅ | ✅ | +| 13 | `NONPUBLIC_STATIC_FIELDS` | ✅ | ⬜️ | ⬜️ | +| 14 | `NONPUBLIC_NONSTATIC_FIELDS_WITH_GETTERS` | ✅ | ⬜️ | ✅ | +| 15 | `NONPUBLIC_NONSTATIC_FIELDS_WITHOUT_GETTERS` | ✅ | ⬜️ | ✅ | +| 16 | `TRANSIENT_FIELDS` | ✅ | ⬜️ | ⬜️ | +| 17 | `STATIC_METHODS` | ✅ | ✅ | ⬜️ | +| 18 | `VOID_METHODS` | ✅ | ✅ | ⬜️ | +| 19 | `GETTER_METHODS` | ✅ | ✅ | ⬜️ | +| 20 | `NONSTATIC_NONVOID_NONGETTER_METHODS` | ✅ | ✅ | ⬜️ | +| 21 | `NULLABLE_FIELDS_BY_DEFAULT` | ✅ | ⬜️ | ⬜️ | +| 22 | `NULLABLE_METHOD_RETURN_VALUES_BY_DEFAULT` | ✅ | ⬜️ | ⬜️ | +| 23 | `NULLABLE_ARRAY_ITEMS_ALLOWED` | ⬜️ | ⬜️ | ⬜️ | +| 24 | `FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS` | ⬜️ | ⬜️ | ⬜️ | +| 25 | `MAP_VALUES_AS_ADDITIONAL_PROPERTIES` | ⬜️ | ⬜️ | ⬜️ | +| 26 | `ENUM_KEYWORD_FOR_SINGLE_VALUES` | ⬜️ | ⬜️ | ⬜️ | +| 27 | `FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT` | ⬜️ | ⬜️ | ⬜️ | +| 28 | `DEFINITIONS_FOR_ALL_OBJECTS` | ⬜️ | ⬜️ | ⬜️ | +| 29 | `DEFINITION_FOR_MAIN_SCHEMA` | ⬜️ | ⬜️ | ⬜️ | +| 30 | `DEFINITIONS_FOR_MEMBER_SUPERTYPES` | ⬜️ | ⬜️ | ⬜️ | +| 31 | `INLINE_ALL_SCHEMAS` | ⬜️ | ⬜️ | ⬜️ | +| 32 | `PLAIN_DEFINITION_KEYS` | ⬜️ | ⬜️ | ⬜️ | +| 33 | `ALLOF_CLEANUP_AT_THE_END` | ✅ | ✅ | ✅ | +| 34 | `STRICT_TYPE_INFO` | ⬜️ | ⬜️ | ⬜️ | diff --git a/slate-docs/source/includes/_main-generator.md b/slate-docs/source/includes/_main-generator.md deleted file mode 100644 index aaa0a852..00000000 --- a/slate-docs/source/includes/_main-generator.md +++ /dev/null @@ -1,1179 +0,0 @@ -The [victools:jsonschema-generator](https://github.com/victools/jsonschema-generator/tree/master/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. - -```java -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 includedBehavior if excluded
1Option.SCHEMA_VERSION_INDICATOR
Setting appropriate $schema attribute on main schema being generated.No $schema attribute is being added.
2Option.ADDITIONAL_FIXED_TYPES
-
    -
  • String/Character/char/CharSequence are treated as { "type": "string" } schema
  • -
  • Boolean/boolean are treated as { "type": "boolean" } schema
  • -
  • Integer/int/Long/long/Short/short/Byte/byte are treated as { "type": "integer" } schema
  • -
  • Double/double/Float/float are treated as { "type": "number" } schema
  • -
  • BigInteger as { "type": "integer" } schema
  • -
  • BigDecimal/Number as { "type": "number" } schema
  • -
  • LocalDate/LocalDateTime/LocalTime/ZonedDateTime/OffsetDateTime/OffsetTime/Instant/Period/ZoneId/Date/Calendar/UUID as { "type": "string" } schema
  • -
-
-
    -
  • String/Character/char/CharSequence are treated as { "type": "string" } schema
  • -
  • Boolean/boolean are treated as { "type": "boolean" } schema
  • -
  • Integer/int/Long/long/Short/short/Byte/byte are treated as { "type": "integer" } schema
  • -
  • Double/double/Float/float are treated as { "type": "number" } schema
  • -
-
3Option.EXTRA_OPEN_API_FORMAT_VALUES
Include extra "format" values (e.g. "int32", "int64", "date", "date-time", "uuid") for fixed types (primitive/basic types, plus some of the Option.ADDITIONAL_FIXED_TYPES if they are enabled as well).no automatic "format" values are being included.
4Option.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.-
#Behavior if includedBehavior if excluded
5Option.FLATTENED_ENUMS
Treating encountered enum types as { "type": "string" } schema with the names of the enum constants being listed as its enum values.-
6Option.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.-
7Option.SIMPLIFIED_OPTIONALS
Treating encountered Optional instances as objects, but including only the get(), orElse() and isPresent() methods.-
8Option.FLATTENED_OPTIONALS
Replacing encountered Optional instances as null-able forms of their generic parameter type.-
#Behavior if includedBehavior if excluded
9Option.FLATTENED_SUPPLIERS
Replacing encountered Supplier instances with their generic parameter type.-
10Option.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.
11Option.PUBLIC_STATIC_FIELDS
Include public static fields in an object's properties.No public static fields are included in an object's properties.
12Option.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.
#Behavior if includedBehavior if excluded
13Option.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.
14Option.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.
15Option.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.
16Option.TRANSIENT_FIELDS
Include transient fields in an object's properties if they would otherwise be included according to the Options above.No transient fields are included in an object's properties even if they would otherwise be included according to the Options above.
#Behavior if includedBehavior if excluded
17Option.STATIC_METHODS
Include public static methods in an object's propertiesNo static methods are included in an object's properties even if they would be included according to the Option.VOID_METHODS below.
18Option.VOID_METHODS
Include public void methods in an object's propertiesNo void methods are included in an object's properties even if they would be included according to the Option.STATIC_METHODS above.
19Option.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.
20Option.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.
#Behavior if includedBehavior if excluded
21Option.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.
22Option.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.
23Option.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.
24Option.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.
#Behavior if includedBehavior if excluded
25Option.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.
26Option.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.
27Option.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.
28Option.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.
#Behavior if includedBehavior if excluded
29Option.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".
30Option.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.
31Option.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.
32Option.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).
#Behavior if includedBehavior if excluded
33Option.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.
34Option.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.
- -Below, you can find the lists of Options included/excluded in the respective standard OptionPresets: - -* "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 | `EXTRA_OPEN_API_FORMAT_VALUES` | ⬜️ | ⬜️ | ⬜️ | -| 4 | `SIMPLIFIED_ENUMS` | ✅ | ✅ | ⬜️ | -| 5 | `FLATTENED_ENUMS` | ⬜️ | ⬜️ | ✅ | -| 6 | `FLATTENED_ENUMS_FROM_TOSTRING` | ⬜️ | ⬜️ | ⬜️ | -| 7 | `SIMPLIFIED_OPTIONALS` | ✅ | ✅ | ⬜️ | -| 8 | `FLATTENED_OPTIONALS` | ⬜️ | ⬜️ | ✅ | -| 8 | `FLATTENED_SUPPLIERS` | ⬜️ | ⬜️ | ✅ | -| 10 | `VALUES_FROM_CONSTANT_FIELDS` | ✅ | ✅ | ✅ | -| 11 | `PUBLIC_STATIC_FIELDS` | ✅ | ✅ | ⬜️ | -| 12 | `PUBLIC_NONSTATIC_FIELDS` | ✅ | ✅ | ✅ | -| 13 | `NONPUBLIC_STATIC_FIELDS` | ✅ | ⬜️ | ⬜️ | -| 14 | `NONPUBLIC_NONSTATIC_FIELDS_WITH_GETTERS` | ✅ | ⬜️ | ✅ | -| 15 | `NONPUBLIC_NONSTATIC_FIELDS_WITHOUT_GETTERS` | ✅ | ⬜️ | ✅ | -| 16 | `TRANSIENT_FIELDS` | ✅ | ⬜️ | ⬜️ | -| 17 | `STATIC_METHODS` | ✅ | ✅ | ⬜️ | -| 18 | `VOID_METHODS` | ✅ | ✅ | ⬜️ | -| 19 | `GETTER_METHODS` | ✅ | ✅ | ⬜️ | -| 20 | `NONSTATIC_NONVOID_NONGETTER_METHODS` | ✅ | ✅ | ⬜️ | -| 21 | `NULLABLE_FIELDS_BY_DEFAULT` | ✅ | ⬜️ | ⬜️ | -| 22 | `NULLABLE_METHOD_RETURN_VALUES_BY_DEFAULT` | ✅ | ⬜️ | ⬜️ | -| 23 | `NULLABLE_ARRAY_ITEMS_ALLOWED` | ⬜️ | ⬜️ | ⬜️ | -| 24 | `FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS` | ⬜️ | ⬜️ | ⬜️ | -| 25 | `MAP_VALUES_AS_ADDITIONAL_PROPERTIES` | ⬜️ | ⬜️ | ⬜️ | -| 26 | `ENUM_KEYWORD_FOR_SINGLE_VALUES` | ⬜️ | ⬜️ | ⬜️ | -| 27 | `FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT` | ⬜️ | ⬜️ | ⬜️ | -| 28 | `DEFINITIONS_FOR_ALL_OBJECTS` | ⬜️ | ⬜️ | ⬜️ | -| 29 | `DEFINITION_FOR_MAIN_SCHEMA` | ⬜️ | ⬜️ | ⬜️ | -| 30 | `DEFINITIONS_FOR_MEMBER_SUPERTYPES` | ⬜️ | ⬜️ | ⬜️ | -| 31 | `INLINE_ALL_SCHEMAS` | ⬜️ | ⬜️ | ⬜️ | -| 32 | `PLAIN_DEFINITION_KEYS` | ⬜️ | ⬜️ | ⬜️ | -| 33 | `ALLOF_CLEANUP_AT_THE_END` | ✅ | ✅ | ✅ | -| 34 | `STRICT_TYPE_INFO` | ⬜️ | ⬜️ | ⬜️ | - -# 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](#generator-individual-configurations) or even [advanced configurations](#generator-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: - -```java -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: - -```java -class MyClass { - public List texts; -} -``` - -> The following schema will be generated: - -```json -{ - "type": "object", - "properties": { - "texts": { - "type": "array", - "title": "texts = (real) List", - "description": "original type = List", - "items": { - "type": "string", - "title": "texts = (fake) String", - "description": "original type = List" - } - } - } -} -``` - -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](https://github.com/victools/jsonschema-generator/tree/master/jsonschema-generator#supported-json-schema-attributes) 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 -```java -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 -```java -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 -```java -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"` -```java -configBuilder.forTypesInGeneral() - .withDefinitionNamingStrategy(new DefaultSchemaDefinitionNamingStrategy() { - @Override - public String getDefinitionNameForKey(DefinitionKey key, SchemaGenerationContext context) { - return super.getDefinitionNameForKey(key, generationContext).toLowerCase(); - } - @Override - public void adjustDuplicateNames(Map 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` and `your.pkg.DateTime` would be represented by `DateTime-1` and `DateTime-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 the `null`able one getting a `"-nullable"` suffix to its definition name. - - - -## Names of fields/methods in an object's `properties` -```java -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 -```java -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` -```java -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 whether `Option.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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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. - -```java -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 registered `AdditionalPropertiesResolver` will be checked. If all return `null`, 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 to `false`. -* If any other type is being returned (e.g. other `Class` or a `ResolvedType`) 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. - -```java -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 registered `AdditionalPropertiesResolver` will be checked. If all return `null`, 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 to `false`. -* 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` 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. - -```java -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` when that is the declared type of a field/method. - -## `"minLength"` Keyword -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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 -```java -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](#generator-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 -```java -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](#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 a `String` or a `Number` but there is no common supertype but `Object` that can be declared: - -```java -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: - -```java -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: - -```json -{ - "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 the `Cat` and `Dog` implementations: - -```java -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](#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): - -```java -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()`: - -```java -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()`: - -```java -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()`: - -```java -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()`: - -```java -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); -``` - - - -1. `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. -2. `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. -3. `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. -4. `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 produce `ResolvedType` instances which are expected by various other methods. - - - -## Custom Property Definitions -```java -// 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](#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()` - - - - \ No newline at end of file diff --git a/slate-docs/source/index.html.md b/slate-docs/source/index.html.md index 025044e4..c90fe296 100644 --- a/slate-docs/source/index.html.md +++ b/slate-docs/source/index.html.md @@ -6,7 +6,10 @@ toc_footers: - Documentation Powered by Slate includes: - - main-generator + - main-generator-options + - main-generator-modules + - main-generator-individual + - main-generator-advanced - jackson-module - jakarta-validation-module - javax-validation-module @@ -34,3 +37,7 @@ This documentation aims at always covering the latest released version of the `j Please refer to the [CHANGELOG](https://github.com/victools/jsonschema-generator/blob/master/CHANGELOG.md) for a list of the incremental changes. *** + +The [victools:jsonschema-generator](https://github.com/victools/jsonschema-generator/tree/master/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). +