diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java index 5cb5b3b8..b6daac2d 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java @@ -286,6 +286,20 @@ public enum Option { * @since 4.11.0 */ PLAIN_DEFINITION_KEYS(null, null), + + /** + * For the "format" attribute, JSON Schema defines various built-in supported values. + *
+ * Some of those data-types would be handed if either {@link #ADDITIONAL_FIXED_TYPES} + * or a custom {@link SimpleTypeModule} (i.e., {@link SimpleTypeModule#forPrimitiveTypes()}) + * are added. + * In that case, by enabling this option these standard built-in "format" values would be added. + * Note that if {@link #EXTRA_OPEN_API_FORMAT_VALUES} is enabled, it overrides this option as + * it include extra "format" attributes. + * + * @since 4.32.1 + */ + STANDARD_FORMATS(null, null), /** * For the "format" attribute, JSON Schema defines various supported values. The OpenAPI specification assigns a few more of those in order to * differentiate between standard data types (e.g. float vs. double) and even some more fixed data types (e.g. LocalDate, LocalDateTime) if @@ -294,7 +308,7 @@ public enum Option { * * @since 4.15.0 */ - EXTRA_OPEN_API_FORMAT_VALUES(null, null), + EXTRA_OPEN_API_FORMAT_VALUES(null, null, Option.STANDARD_FORMATS), /** * Whether as the last step of the schema generation, unnecessary "allOf" elements (i.e. where there are no conflicts/overlaps between the * contained sub-schemas) should be merged into one, in order to make the generated schema more readable. This also applies to manually added diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java index 27957447..2007c69f 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java @@ -99,6 +99,13 @@ public interface SchemaGeneratorConfig extends StatefulConfig { */ boolean shouldUsePlainDefinitionKeys(); + /** + * Determine whether standard {@link SchemaKeyword#TAG_FORMAT} values should be included for "simple types". + * + * @return whether to include standard {@link SchemaKeyword#TAG_FORMAT} values + */ + boolean shouldIncludeStandardFormatValues(); + /** * Determine whether extra {@link SchemaKeyword#TAG_FORMAT} values should be included for "simple types". * diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java index b109dc99..02e82e8c 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java @@ -138,6 +138,11 @@ public boolean shouldUsePlainDefinitionKeys() { return this.isOptionEnabled(Option.PLAIN_DEFINITION_KEYS); } + @Override + public boolean shouldIncludeStandardFormatValues() { + return this.isOptionEnabled(Option.STANDARD_FORMATS); + } + @Override public boolean shouldIncludeExtraOpenApiFormatValues() { return this.isOptionEnabled(Option.EXTRA_OPEN_API_FORMAT_VALUES); diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModule.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModule.java index 44cd8ab8..7262b665 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModule.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModule.java @@ -27,8 +27,10 @@ import com.github.victools.jsonschema.generator.SchemaKeyword; import com.github.victools.jsonschema.generator.TypeScope; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -81,17 +83,18 @@ public static SimpleTypeModule forPrimitiveTypes() { public static SimpleTypeModule forPrimitiveAndAdditionalTypes() { SimpleTypeModule module = SimpleTypeModule.forPrimitiveTypes(); - module.withStringType(java.time.LocalDate.class, "date"); + module.withStandardStringType(java.time.LocalDate.class, "date"); Stream.of(java.time.LocalDateTime.class, java.time.ZonedDateTime.class, java.time.OffsetDateTime.class, java.time.Instant.class, java.util.Date.class, java.util.Calendar.class) - .forEach(javaType -> module.withStringType(javaType, "date-time")); + .forEach(javaType -> module.withStandardStringType(javaType, "date-time")); Stream.of(java.time.LocalTime.class, java.time.OffsetTime.class) - .forEach(javaType -> module.withStringType(javaType, "time")); - module.withStringType(java.util.UUID.class, "uuid"); - module.withStringType(java.net.URI.class, "uri"); + .forEach(javaType -> module.withStandardStringType(javaType, "time")); + Stream.of(java.time.Duration.class, java.time.Period.class) + .forEach(javaType -> module.withStandardStringType(javaType, "duration")); + module.withStandardStringType(java.util.UUID.class, "uuid"); + module.withStandardStringType(java.net.URI.class, "uri"); module.withStringType(java.time.ZoneId.class); - module.withStringType(java.time.Period.class); module.withIntegerType(java.math.BigInteger.class); Stream.of(java.math.BigDecimal.class, Number.class) @@ -101,6 +104,7 @@ public static SimpleTypeModule forPrimitiveAndAdditionalTypes() { } private final Map, SchemaKeyword> fixedJsonSchemaTypes = new HashMap<>(); + private final List> standardFormats = new ArrayList<>(); private final Map, String> extraOpenApiFormatValues = new HashMap<>(); /** @@ -162,6 +166,12 @@ public final SimpleTypeModule withStringType(Class javaType, String openApiFo return this.with(javaType, SchemaKeyword.TAG_TYPE_STRING, openApiFormat); } + private final SimpleTypeModule withStandardStringType(Class javaType, final String format) { + // track as a standard format + this.standardFormats.add(javaType); + return this.withStringType(javaType, format); + } + /** * Add the given mapping for a (simple) java class to its JSON schema equivalent "type" attribute: "{@link SchemaKeyword#TAG_TYPE_BOOLEAN}". * @@ -286,7 +296,7 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch if (jsonSchemaTypeValue != SchemaKeyword.TAG_TYPE_NULL) { customSchema.put(context.getKeyword(SchemaKeyword.TAG_TYPE), context.getKeyword(jsonSchemaTypeValue)); } - if (context.getGeneratorConfig().shouldIncludeExtraOpenApiFormatValues()) { + if (shouldAddFormatTag(javaType, context)) { String formatValue = SimpleTypeModule.this.extraOpenApiFormatValues.get(javaType.getErasedType()); if (formatValue != null) { customSchema.put(context.getKeyword(SchemaKeyword.TAG_FORMAT), formatValue); @@ -295,5 +305,12 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch // set true as second parameter to indicate simple types to be always in-lined (i.e. not put into definitions) return new CustomDefinition(customSchema, CustomDefinition.DefinitionType.INLINE, CustomDefinition.AttributeInclusion.YES); } + + private boolean shouldAddFormatTag(final ResolvedType javaType, final SchemaGenerationContext context) { + // either OpenAPI extra formats or standard-formats that are registered + return context.getGeneratorConfig().shouldIncludeExtraOpenApiFormatValues() + || (context.getGeneratorConfig().shouldIncludeStandardFormatValues() + && standardFormats.contains(javaType.getErasedType())); + } } } diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java index 02ae9ddf..54893ba7 100644 --- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java +++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java @@ -32,40 +32,43 @@ public class SchemaGeneratorSimpleTypesTest { static Stream getSimpleTypeCombinations() { + // order of arguments: targetType, expectedJsonSchemaType, expectedOpenApiFormat, standardFormat return Stream.of( - Arguments.of(Object.class, null, null), - Arguments.of(String.class, SchemaKeyword.TAG_TYPE_STRING, null), - Arguments.of(Character.class, SchemaKeyword.TAG_TYPE_STRING, null), - Arguments.of(char.class, SchemaKeyword.TAG_TYPE_STRING, null), - Arguments.of(CharSequence.class, SchemaKeyword.TAG_TYPE_STRING, null), - Arguments.of(Byte.class, SchemaKeyword.TAG_TYPE_STRING, null), - Arguments.of(byte.class, SchemaKeyword.TAG_TYPE_STRING, null), - Arguments.of(Boolean.class, SchemaKeyword.TAG_TYPE_BOOLEAN, null), - Arguments.of(boolean.class, SchemaKeyword.TAG_TYPE_BOOLEAN, null), - Arguments.of(Integer.class, SchemaKeyword.TAG_TYPE_INTEGER, "int32"), - Arguments.of(int.class, SchemaKeyword.TAG_TYPE_INTEGER, "int32"), - Arguments.of(Long.class, SchemaKeyword.TAG_TYPE_INTEGER, "int64"), - Arguments.of(long.class, SchemaKeyword.TAG_TYPE_INTEGER, "int64"), - Arguments.of(Short.class, SchemaKeyword.TAG_TYPE_INTEGER, null), - Arguments.of(short.class, SchemaKeyword.TAG_TYPE_INTEGER, null), - Arguments.of(Double.class, SchemaKeyword.TAG_TYPE_NUMBER, "double"), - Arguments.of(double.class, SchemaKeyword.TAG_TYPE_NUMBER, "double"), - Arguments.of(Float.class, SchemaKeyword.TAG_TYPE_NUMBER, "float"), - Arguments.of(float.class, SchemaKeyword.TAG_TYPE_NUMBER, "float"), - Arguments.of(java.time.LocalDate.class, SchemaKeyword.TAG_TYPE_STRING, "date"), - Arguments.of(java.time.LocalDateTime.class, SchemaKeyword.TAG_TYPE_STRING, "date-time"), - Arguments.of(java.time.LocalTime.class, SchemaKeyword.TAG_TYPE_STRING, "time"), - Arguments.of(java.time.ZonedDateTime.class, SchemaKeyword.TAG_TYPE_STRING, "date-time"), - Arguments.of(java.time.OffsetDateTime.class, SchemaKeyword.TAG_TYPE_STRING, "date-time"), - Arguments.of(java.time.OffsetTime.class, SchemaKeyword.TAG_TYPE_STRING, "time"), - Arguments.of(java.time.Instant.class, SchemaKeyword.TAG_TYPE_STRING, "date-time"), - Arguments.of(java.util.Date.class, SchemaKeyword.TAG_TYPE_STRING, "date-time"), - Arguments.of(java.util.Calendar.class, SchemaKeyword.TAG_TYPE_STRING, "date-time"), - Arguments.of(java.util.UUID.class, SchemaKeyword.TAG_TYPE_STRING, "uuid"), - Arguments.of(java.time.ZoneId.class, SchemaKeyword.TAG_TYPE_STRING, null), - Arguments.of(java.math.BigInteger.class, SchemaKeyword.TAG_TYPE_INTEGER, null), - Arguments.of(java.math.BigDecimal.class, SchemaKeyword.TAG_TYPE_NUMBER, null), - Arguments.of(Number.class, SchemaKeyword.TAG_TYPE_NUMBER, null) + Arguments.of(Object.class, null, null, null), + Arguments.of(String.class, SchemaKeyword.TAG_TYPE_STRING, null, null), + Arguments.of(Character.class, SchemaKeyword.TAG_TYPE_STRING, null, null), + Arguments.of(char.class, SchemaKeyword.TAG_TYPE_STRING, null, null), + Arguments.of(CharSequence.class, SchemaKeyword.TAG_TYPE_STRING, null, null), + Arguments.of(Byte.class, SchemaKeyword.TAG_TYPE_STRING, null, null), + Arguments.of(byte.class, SchemaKeyword.TAG_TYPE_STRING, null, null), + Arguments.of(Boolean.class, SchemaKeyword.TAG_TYPE_BOOLEAN, null, null), + Arguments.of(boolean.class, SchemaKeyword.TAG_TYPE_BOOLEAN, null, null), + Arguments.of(Integer.class, SchemaKeyword.TAG_TYPE_INTEGER, "int32", null), + Arguments.of(int.class, SchemaKeyword.TAG_TYPE_INTEGER, "int32", null), + Arguments.of(Long.class, SchemaKeyword.TAG_TYPE_INTEGER, "int64", null), + Arguments.of(long.class, SchemaKeyword.TAG_TYPE_INTEGER, "int64", null), + Arguments.of(Short.class, SchemaKeyword.TAG_TYPE_INTEGER, null, null), + Arguments.of(short.class, SchemaKeyword.TAG_TYPE_INTEGER, null, null), + Arguments.of(Double.class, SchemaKeyword.TAG_TYPE_NUMBER, "double", null, null), + Arguments.of(double.class, SchemaKeyword.TAG_TYPE_NUMBER, "double", null, null), + Arguments.of(Float.class, SchemaKeyword.TAG_TYPE_NUMBER, "float", null, null), + Arguments.of(float.class, SchemaKeyword.TAG_TYPE_NUMBER, "float", null, null), + Arguments.of(java.time.LocalDate.class, SchemaKeyword.TAG_TYPE_STRING, "date", "date"), + Arguments.of(java.time.LocalDateTime.class, SchemaKeyword.TAG_TYPE_STRING, "date-time", "date-time"), + Arguments.of(java.time.LocalTime.class, SchemaKeyword.TAG_TYPE_STRING, "time", "time"), + Arguments.of(java.time.ZonedDateTime.class, SchemaKeyword.TAG_TYPE_STRING, "date-time", "date-time"), + Arguments.of(java.time.OffsetDateTime.class, SchemaKeyword.TAG_TYPE_STRING, "date-time", "date-time"), + Arguments.of(java.time.OffsetTime.class, SchemaKeyword.TAG_TYPE_STRING, "time", "time"), + Arguments.of(java.time.Instant.class, SchemaKeyword.TAG_TYPE_STRING, "date-time", "date-time"), + Arguments.of(java.util.Date.class, SchemaKeyword.TAG_TYPE_STRING, "date-time", "date-time"), + Arguments.of(java.util.Calendar.class, SchemaKeyword.TAG_TYPE_STRING, "date-time", "date-time"), + Arguments.of(java.time.Duration.class, SchemaKeyword.TAG_TYPE_STRING, "duration", "duration"), + Arguments.of(java.time.Period.class, SchemaKeyword.TAG_TYPE_STRING, "duration", "duration"), + Arguments.of(java.util.UUID.class, SchemaKeyword.TAG_TYPE_STRING, "uuid", "uuid"), + Arguments.of(java.time.ZoneId.class, SchemaKeyword.TAG_TYPE_STRING, null, null), + Arguments.of(java.math.BigInteger.class, SchemaKeyword.TAG_TYPE_INTEGER, null, null), + Arguments.of(java.math.BigDecimal.class, SchemaKeyword.TAG_TYPE_NUMBER, null, null), + Arguments.of(Number.class, SchemaKeyword.TAG_TYPE_NUMBER, null, null) ); } @@ -83,6 +86,13 @@ static Stream parametersForTestGenerateSchema_SimpleTypeWithFormat() .map(entry -> Arguments.of(entry.get()[0], entry.get()[1], entry.get()[2], schemaVersion))); } + static Stream parametersForTestGenerateSchema_SimpleTypeWithStandardFormat() { + List typeCombinations = getSimpleTypeCombinations().collect(Collectors.toList()); + return EnumSet.allOf(SchemaVersion.class).stream() + .flatMap(schemaVersion -> typeCombinations.stream() + .map(entry -> Arguments.of(entry.get()[0], entry.get()[1], entry.get()[3], schemaVersion))); + } + @ParameterizedTest @MethodSource("parametersForTestGenerateSchema_SimpleTypeWithoutFormat") public void testGenerateSchema_SimpleTypeWithoutFormat(Class targetType, SchemaKeyword expectedJsonSchemaType, SchemaVersion schemaVersion) @@ -141,4 +151,27 @@ public void testGenerateSchema_SimpleType_withAdditionalPropertiesOption(Class targetType, SchemaKeyword expectedJsonSchemaType, String expectedFormat, + SchemaVersion schemaVersion) throws Exception { + SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(schemaVersion) + .with(Option.ADDITIONAL_FIXED_TYPES, Option.STANDARD_FORMATS) + .build(); + SchemaGenerator generator = new SchemaGenerator(config); + JsonNode result = generator.generateSchema(targetType); + if (expectedJsonSchemaType == null) { + Assertions.assertTrue(result.isEmpty()); + } else if (expectedFormat == null) { + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(expectedJsonSchemaType.forVersion(schemaVersion), + result.get(SchemaKeyword.TAG_TYPE.forVersion(schemaVersion)).asText()); + } else { + Assertions.assertEquals(2, result.size()); + Assertions.assertEquals(expectedJsonSchemaType.forVersion(schemaVersion), + result.get(SchemaKeyword.TAG_TYPE.forVersion(schemaVersion)).asText()); + Assertions.assertEquals(expectedFormat, result.get(SchemaKeyword.TAG_FORMAT.forVersion(schemaVersion)).asText()); + } + } } diff --git a/slate-docs/source/includes/_main-generator-options.md b/slate-docs/source/includes/_main-generator-options.md index c2111135..fe02215e 100644 --- a/slate-docs/source/includes/_main-generator-options.md +++ b/slate-docs/source/includes/_main-generator-options.md @@ -61,7 +61,7 @@ configBuilder.without( Option.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). + Include extra "format" values (e.g. "int32", "int64", "date", "time", "date-time", "duration", "uuid", "uri") for fixed types (primitive/basic types, plus some of the Option.ADDITIONAL_FIXED_TYPES if they are enabled as well). Only works if Option.ADDITIONAL_FIXED_TYPES is set and it overrides Option.STANDARD_FORMATS. no automatic "format" values are being included. @@ -323,6 +323,15 @@ configBuilder.without( 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. + + 35 + Option.STANDARD_FORMATS + + + Same as Option.EXTRA_OPEN_API_FORMAT_VALUES but only for built-in supported "format" values ("date", "time", "date-time", "duration", "uuid", "uri"). + Only works if Option.ADDITIONAL_FIXED_TYPES is set and it is overriden by Option.EXTRA_OPEN_API_FORMAT_VALUES + no automatic "format" values are being included. + @@ -368,3 +377,4 @@ Below, you can find the lists of Options included/excluded in the r | 32 | `PLAIN_DEFINITION_KEYS` | ⬜️ | ⬜️ | ⬜️ | | 33 | `ALLOF_CLEANUP_AT_THE_END` | ✅ | ✅ | ✅ | | 34 | `STRICT_TYPE_INFO` | ⬜️ | ⬜️ | ⬜️ | +| 35 | `STANDARD_FORMATS` | ⬜️ | ⬜️ | ⬜️ |