Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: option to inline nullable schemas #412

Merged
merged 1 commit into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### `jsonschema-generator`
#### Added
- new `Option.STANDARD_FORMATS` includes standard `"format"` values to some types considered by `Option.ADDITIONAL_FIXED_TYPES`
- new `Option.INLINE_NULLABLE_SCHEMAS` avoids `"<type>-nullable"` entries in the `"definitions"`/`"$defs"`

#### Changed
- include new `Option.STANDARD_FORMATS` in `OptionPreset.PLAIN_JSON` by default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,14 @@ public enum Option {
* @since 4.27.0
*/
DEFINITIONS_FOR_MEMBER_SUPERTYPES(null, null),
/**
* Whether the "nullable" variants of a sub-schema should be defined in-line, i.e., avoiding a second "MyType-nullable" entry in the
* "definitions"/"$defs". This takes precedence over {@link #DEFINITIONS_FOR_ALL_OBJECTS} in this specific case. The non-nullable sub-schema is
* unaffected by this setting.
*
* @since 4.33.0
*/
INLINE_NULLABLE_SCHEMAS(null, null),
/**
* Whether all sub-schemas should be defined in-line, i.e. including no "definitions"/"$defs". This takes precedence over
* {@link #DEFINITIONS_FOR_ALL_OBJECTS} and {@link #DEFINITION_FOR_MAIN_SCHEMA}.
Expand All @@ -277,7 +285,8 @@ public enum Option {
*
* @since 4.10.0
*/
INLINE_ALL_SCHEMAS(InlineSchemaModule::new, null, Option.DEFINITIONS_FOR_ALL_OBJECTS, Option.DEFINITION_FOR_MAIN_SCHEMA),
INLINE_ALL_SCHEMAS(InlineSchemaModule::new, null,
Option.DEFINITIONS_FOR_ALL_OBJECTS, Option.DEFINITION_FOR_MAIN_SCHEMA, Option.INLINE_NULLABLE_SCHEMAS),
/**
* Generally, keys in the collected "definitions"/"$defs" are ensured to be URI compatible but may include parentheses and commas for listing type
* parameters. By enabling this option, these parentheses and commas will be removed to conform with a reduced set of characters, e.g. as expected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,17 +205,14 @@ private void performCleanup() {
private ObjectNode buildDefinitionsAndResolveReferences(String designatedDefinitionPath, DefinitionKey mainSchemaKey,
SchemaGenerationContextImpl generationContext) {
final ObjectNode definitionsNode = this.config.createObjectNode();
final boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects();
CarstenWickner marked this conversation as resolved.
Show resolved Hide resolved
final boolean inlineAllSchemas = this.config.shouldInlineAllSchemas();
CarstenWickner marked this conversation as resolved.
Show resolved Hide resolved

final AtomicBoolean considerOnlyDirectReferences = new AtomicBoolean(false);
Predicate<DefinitionKey> shouldProduceDefinition = this.getShouldProduceDefinitionCheck(mainSchemaKey, considerOnlyDirectReferences,
createDefinitionsForAll, inlineAllSchemas);
Predicate<DefinitionKey> shouldProduceDefinition = this.getShouldProduceDefinitionCheck(mainSchemaKey, considerOnlyDirectReferences);
Map<DefinitionKey, String> baseReferenceKeys = this.getReferenceKeys(mainSchemaKey, shouldProduceDefinition, generationContext);
considerOnlyDirectReferences.set(true);
for (Map.Entry<DefinitionKey, String> entry : baseReferenceKeys.entrySet()) {
String definitionName = entry.getValue();
DefinitionKey definitionKey = entry.getKey();
for (Map.Entry<DefinitionKey, String> baseReferenceKey : baseReferenceKeys.entrySet()) {
String definitionName = baseReferenceKey.getValue();
DefinitionKey definitionKey = baseReferenceKey.getKey();
List<ObjectNode> references = generationContext.getReferences(definitionKey);
List<ObjectNode> nullableReferences = generationContext.getNullableReferences(definitionKey);
final String referenceKey;
Expand All @@ -242,8 +239,7 @@ private ObjectNode buildDefinitionsAndResolveReferences(String designatedDefinit
definition = this.config.createObjectNode().put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey);
}
generationContext.makeNullable(definition);
if (generationContext.shouldNeverInlineDefinition(definitionKey)
|| (!inlineAllSchemas && (createDefinitionsForAll || nullableReferences.size() > 1))) {
CarstenWickner marked this conversation as resolved.
Show resolved Hide resolved
if (this.shouldCreateNullableDefinition(generationContext, definitionKey, nullableReferences)) {
String nullableDefinitionName = this.definitionNamingStrategy
.adjustNullableName(definitionKey, definitionName, generationContext);
definitionsNode.set(nullableDefinitionName, definition);
Expand All @@ -258,6 +254,20 @@ private ObjectNode buildDefinitionsAndResolveReferences(String designatedDefinit
return definitionsNode;
}

private boolean shouldCreateNullableDefinition(SchemaGenerationContextImpl generationContext, DefinitionKey definitionKey,
List<ObjectNode> nullableReferences) {
if (this.config.shouldInlineNullableSchemas()) {
return false;
}
if (generationContext.shouldNeverInlineDefinition(definitionKey)) {
return true;
}
if (this.config.shouldInlineAllSchemas()) {
return false;
}
return this.config.shouldCreateDefinitionsForAllObjects() || nullableReferences.size() > 1;
}

private String getReferenceKey(DefinitionKey mainSchemaKey, DefinitionKey definitionKey, Supplier<String> addDefinitionAndReturnReferenceKey) {
final String referenceKey;
if (definitionKey.equals(mainSchemaKey) && !this.config.shouldCreateDefinitionForMainSchema()) {
Expand All @@ -275,12 +285,11 @@ private String getReferenceKey(DefinitionKey mainSchemaKey, DefinitionKey defini
*
* @param mainSchemaKey main type to consider
* @param considerOnlyDirectReferences whether to ignore nullable references when determing about definition vs. inlining
* @param createDefinitionsForAll whether to produce definitions for all schemas by default (unless explicitly stated otherwise)
* @param inlineAllSchemas whether to inline all schemas by default (unless explicitly stated otherwise)
* @return reusable predicate
*/
private Predicate<DefinitionKey> getShouldProduceDefinitionCheck(DefinitionKey mainSchemaKey, AtomicBoolean considerOnlyDirectReferences,
boolean createDefinitionsForAll, boolean inlineAllSchemas) {
private Predicate<DefinitionKey> getShouldProduceDefinitionCheck(DefinitionKey mainSchemaKey, AtomicBoolean considerOnlyDirectReferences) {
final boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects();
final boolean inlineAllSchemas = this.config.shouldInlineAllSchemas();
return definitionKey -> {
if (generationContext.shouldNeverInlineDefinition(definitionKey)) {
// e.g. custom definition explicitly marked to always produce a definition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ public interface SchemaGeneratorConfig extends StatefulConfig {
*/
boolean shouldInlineAllSchemas();

/**
* Determine whether nullable sub-schemas should be included in-line, even if they occur multiple times, and not in the schema's
* "definitions"/"$defs".
*
* @return whether to include nullable sub-schemas in-line
*/
boolean shouldInlineNullableSchemas();

/**
* Determine whether the {@link SchemaKeyword#TAG_SCHEMA} attribute with {@link SchemaKeyword#TAG_SCHEMA_VALUE} should be added.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ public boolean shouldInlineAllSchemas() {
return this.isOptionEnabled(Option.INLINE_ALL_SCHEMAS);
}

@Override
public boolean shouldInlineNullableSchemas() {
return this.isOptionEnabled(Option.INLINE_NULLABLE_SCHEMAS);
}

@Override
public boolean shouldUsePlainDefinitionKeys() {
return this.isOptionEnabled(Option.PLAIN_DEFINITION_KEYS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ static Stream<Arguments> parametersForTestGenerateSchema() {
return null;
});
Module alternativeDefinitionModule = configBuilder -> configBuilder.with(Option.DEFINITION_FOR_MAIN_SCHEMA,
Option.PLAIN_DEFINITION_KEYS, Option.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS);
Option.PLAIN_DEFINITION_KEYS, Option.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS, Option.INLINE_NULLABLE_SCHEMAS);
Module typeInGeneralModule = configBuilder -> populateTypeConfigPart(
configBuilder.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT).forTypesInGeneral()
.withIdResolver(scope -> scope.getType().getTypeName().contains("$Test") ? "id-" + scope.getSimpleTypeDescription() : null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public void testGenerateSchema_CircularCustomStandardDefinition(SchemaVersion sc
.set(accessProperty, context.createDefinitionReference(generic))));
};
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(schemaVersion);
configBuilder.with(Option.INLINE_NULLABLE_SCHEMAS);
configBuilder.forTypesInGeneral()
.withCustomDefinitionProvider(customDefinitionProvider);
SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
{
"$defs": {
"List(TestCircularClass1)-nullable": {
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#"
}
}
},
"List(TestCircularClass2)-nullable": {
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#/$defs/TestCircularClass2"
}
}
},
"TestCircularClass2": {
"type": "object",
"properties": {
"list1": {
"$ref": "#/$defs/List(TestCircularClass1)-nullable"
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#"
}
}
}
}
}
},
"type": "object",
"properties": {
"list2": {
"$ref": "#/$defs/List(TestCircularClass2)-nullable"
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#/$defs/TestCircularClass2"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
{
"$defs": {
"List(TestCircularClass1)-nullable": {
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#"
}
}
},
"List(TestCircularClass2)-nullable": {
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#/$defs/TestCircularClass2"
}
}
},
"TestCircularClass2": {
"type": "object",
"properties": {
"list1": {
"$ref": "#/$defs/List(TestCircularClass1)-nullable"
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#"
}
}
}
}
}
},
"type": "object",
"properties": {
"list2": {
"$ref": "#/$defs/List(TestCircularClass2)-nullable"
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#/$defs/TestCircularClass2"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
{
"definitions": {
"List(TestCircularClass1)-nullable": {
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#"
}
}
},
"List(TestCircularClass2)-nullable": {
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#/definitions/TestCircularClass2"
}
}
},
"TestCircularClass2": {
"type": "object",
"properties": {
"list1": {
"$ref": "#/definitions/List(TestCircularClass1)-nullable"
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#"
}
}
}
}
}
},
"type": "object",
"properties": {
"list2": {
"$ref": "#/definitions/List(TestCircularClass2)-nullable"
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#/definitions/TestCircularClass2"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
{
"definitions": {
"List(TestCircularClass1)-nullable": {
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#"
}
}
},
"List(TestCircularClass2)-nullable": {
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#/definitions/TestCircularClass2"
}
}
},
"TestCircularClass2": {
"type": "object",
"properties": {
"list1": {
"$ref": "#/definitions/List(TestCircularClass1)-nullable"
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#"
}
}
}
}
}
},
"type": "object",
"properties": {
"list2": {
"$ref": "#/definitions/List(TestCircularClass2)-nullable"
"type": ["object", "null"],
"properties": {
"get(0)": {
"$ref": "#/definitions/TestCircularClass2"
}
}
}
}
}
Loading
Loading