Skip to content

Commit

Permalink
feat: option to inline nullable schemas (#412)
Browse files Browse the repository at this point in the history
  • Loading branch information
CarstenWickner committed Nov 18, 2023
1 parent 630aae2 commit 1044f45
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 208 deletions.
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();
final boolean inlineAllSchemas = this.config.shouldInlineAllSchemas();

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))) {
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

0 comments on commit 1044f45

Please sign in to comment.