Repository: victools/jsonschema-generator
Pull request #143 (2020-09-25 21:37:25)

feat: introduce Option.ENUM_KEYWORD_FOR_SINGLE_VALUES
Implementing new `Option` for the feature requested in #142.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef1396e..5e12d4c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### `jsonschema-generator`
+#### Added
+- New `Option.ENUM_KEYWORD_FOR_SINGLE_VALUES` to produce `"enum": ["A"]` instead of `"const": "A"` if there is only one allowed value.
 
 ## [4.15.1] - 2020-09-15
 ### `jsonschema-generator`
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 894f7e3..0ef83ba 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
@@ -205,11 +205,18 @@ public enum Option {
      * @since 4.11.0
      */
     MAP_VALUES_AS_ADDITIONAL_PROPERTIES(AdditionalPropertiesModule::forMapValues, null),
+    /**
+     * Whether allowed values should always be included in an {@link SchemaKeyword#TAG_ENUM "enum"} keyword. If there is exactly one allowed value, it
+     * will otherwise be represented by a {@link SchemaKeyword#TAG_CONST "const"} keyword instead.
+     *
+     * @since 4.16.0
+     */
+    ENUM_KEYWORD_FOR_SINGLE_VALUES(null, null),
     /**
      * Whether a schema's "additionalProperties" should be set to "false" if no specific configuration says otherwise.
      * <br>
-     * Without this option, i.e. by default the "additionalProperties" keyword will be omitted and thereby allowing any additional properties in an
-     * object schema).
+     * Without this option, i.e. by default, the "additionalProperties" keyword will be omitted and thereby allowing any additional properties in an
+     * object schema.
      *
      * @since 4.2.0
      */
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 1241914..5860c47 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
@@ -122,6 +122,14 @@ public interface SchemaGeneratorConfig {
      */
     boolean shouldDeriveFieldsFromArgumentFreeMethods();
 
+    /**
+     * Determine whether a single allowed values should be represented by a {@link SchemaKeyword#TAG_CONST}. Otherwise a
+     * {@link SchemaKeyword#TAG_ENUM} will be used even for a single allowed value.
+     *
+     * @return whether to automatically make use of the {@link SchemaKeyword#TAG_CONST "const"} keyword
+     */
+    boolean shouldRepresentSingleAllowedValueAsConst();
+
     /**
      * Getter for the underlying object mapper.
      *
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
index cdaa873..9f04e11 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
@@ -404,7 +404,7 @@ public class AttributeCollector {
                     .filter(this::isSupportedEnumValue)
                     .filter(this::canBeConvertedToString)
                     .collect(Collectors.toList());
-            if (values.size() == 1) {
+            if (values.size() == 1 && generationContext.getGeneratorConfig().shouldRepresentSingleAllowedValueAsConst()) {
                 Object singleValue = values.get(0);
                 if (singleValue instanceof String) {
                     node.put(generationContext.getKeyword(SchemaKeyword.TAG_CONST), (String) singleValue);
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 072cd73..abec3e3 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
@@ -149,6 +149,11 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
         return this.isOptionEnabled(Option.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS);
     }
 
+    @Override
+    public boolean shouldRepresentSingleAllowedValueAsConst() {
+        return !this.isOptionEnabled(Option.ENUM_KEYWORD_FOR_SINGLE_VALUES);
+    }
+
     @Override
     public boolean shouldIncludeSchemaVersionIndicator() {
         return this.isOptionEnabled(Option.SCHEMA_VERSION_INDICATOR);


Test patch:
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
index c3ca881..9918160 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
@@ -20,6 +20,7 @@ import com.fasterxml.classmate.ResolvedType;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.Option;
 import com.github.victools.jsonschema.generator.SchemaGenerationContext;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
@@ -27,8 +28,11 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart
 import com.github.victools.jsonschema.generator.SchemaKeyword;
 import com.github.victools.jsonschema.generator.SchemaVersion;
 import java.lang.reflect.Type;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import junitparams.JUnitParamsRunner;
 import junitparams.Parameters;
 import org.junit.Assert;
@@ -59,17 +63,67 @@ public class AttributeCollectorTest {
      * @param schemaVersion designated JSON Schema version
      * @return schema generation context for the given JSON Schema version
      */
-    private SchemaGenerationContextImpl generateContext(SchemaVersion schemaVersion) {
+    private SchemaGenerationContextImpl generateContext(SchemaVersion schemaVersion, Option... options) {
         SchemaGeneratorConfig generatorConfig = new SchemaGeneratorConfigImpl(
                 this.objectMapper,
                 schemaVersion,
-                Collections.emptySet(),
+                Stream.of(options).collect(Collectors.toSet()),
                 new SchemaGeneratorGeneralConfigPart(),
                 new SchemaGeneratorConfigPart<>(),
                 new SchemaGeneratorConfigPart<>());
         return new SchemaGenerationContextImpl(generatorConfig, TypeContextFactory.createDefaultTypeContext());
     }
 
+    @Test
+    @Parameters(source = SchemaVersion.class)
+    public void testSetEnum_Null(SchemaVersion schemaVersion) {
+        this.collector.setEnum(this.definitionNode, null, this.generateContext(schemaVersion));
+        Assert.assertTrue(this.definitionNode.isEmpty());
+    }
+
+    @Test
+    @Parameters(source = SchemaVersion.class)
+    public void testSetEnum_singleConstValue(SchemaVersion schemaVersion) {
+        this.collector.setEnum(this.definitionNode, Arrays.asList("A"), this.generateContext(schemaVersion));
+        JsonNode constNode = this.definitionNode.get(SchemaKeyword.TAG_CONST.forVersion(schemaVersion));
+        Assert.assertNotNull(constNode);
+        Assert.assertTrue(constNode.isTextual());
+        Assert.assertEquals("A", constNode.textValue());
+    }
+
+    @Test
+    @Parameters(source = SchemaVersion.class)
+    public void testSetEnum_singleEnumValue(SchemaVersion schemaVersion) {
+        this.collector.setEnum(this.definitionNode, Arrays.asList("A"), this.generateContext(schemaVersion, Option.ENUM_KEYWORD_FOR_SINGLE_VALUES));
+        JsonNode enumNode = this.definitionNode.get(SchemaKeyword.TAG_ENUM.forVersion(schemaVersion));
+        Assert.assertNotNull(enumNode);
+        Assert.assertTrue(enumNode.isArray());
+        Assert.assertEquals(1, enumNode.size());
+        JsonNode singleValueItem = enumNode.get(0);
+        Assert.assertNotNull(singleValueItem);
+        Assert.assertTrue(singleValueItem.isTextual());
+        Assert.assertEquals("A", singleValueItem.textValue());
+    }
+
+    @Test
+    @Parameters({"default", "ENUM_KEYWORD_FOR_SINGLE_VALUES"})
+    public void testSetEnum_twoEnumValues(String optionMode) {
+        Option[] options = optionMode.equals("default") ? new Option[0] : new Option[]{Option.ENUM_KEYWORD_FOR_SINGLE_VALUES};
+        this.collector.setEnum(this.definitionNode, Arrays.asList("A", "B"), this.generateContext(SchemaVersion.DRAFT_2019_09, options));
+        JsonNode enumNode = this.definitionNode.get(SchemaKeyword.TAG_ENUM.forVersion(SchemaVersion.DRAFT_2019_09));
+        Assert.assertNotNull(enumNode);
+        Assert.assertTrue(enumNode.isArray());
+        Assert.assertEquals(2, enumNode.size());
+        JsonNode firstValueItem = enumNode.get(0);
+        Assert.assertNotNull(firstValueItem);
+        Assert.assertTrue(firstValueItem.isTextual());
+        Assert.assertEquals("A", firstValueItem.textValue());
+        JsonNode secondValueItem = enumNode.get(1);
+        Assert.assertNotNull(secondValueItem);
+        Assert.assertTrue(secondValueItem.isTextual());
+        Assert.assertEquals("B", secondValueItem.textValue());
+    }
+
     @Test
     @Parameters(source = SchemaVersion.class)
     public void testSetAdditionalProperties_Null(SchemaVersion schemaVersion) {
diff --git a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProviderTest.java b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProviderTest.java
index 28debee..6601f96 100644
--- a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProviderTest.java
+++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProviderTest.java
@@ -72,8 +72,10 @@ public class CustomEnumDefinitionProviderTest {
     }
 
     @Test
-    @Parameters
-    public void testProvideCustomSchemaDefinition(Class<?> erasedType, boolean considerJsonValue, boolean considerJsonProperty, List<String> values) {
+    @Parameters(method = "parametersForTestProvideCustomSchemaDefinition")
+    public void testProvideCustomSchemaDefinition_withConst(Class<?> erasedType, boolean considerJsonValue, boolean considerJsonProperty,
+            List<String> values) {
+        Mockito.when(this.generationContext.getGeneratorConfig().shouldRepresentSingleAllowedValueAsConst()).thenReturn(true);
         ResolvedType type = this.typeContext.resolve(erasedType);
         CustomDefinition result = new CustomEnumDefinitionProvider(considerJsonValue, considerJsonProperty)
                 .provideCustomSchemaDefinition(type, this.generationContext);
@@ -99,6 +101,30 @@ public class CustomEnumDefinitionProviderTest {
         }
     }
 
+    @Test
+    @Parameters(method = "parametersForTestProvideCustomSchemaDefinition")
+    public void testProvideCustomSchemaDefinition_withoutConst(Class<?> erasedType, boolean considerJsonValue, boolean considerJsonProperty,
+            List<String> values) {
+        Mockito.when(this.generationContext.getGeneratorConfig().shouldRepresentSingleAllowedValueAsConst()).thenReturn(false);
+        ResolvedType type = this.typeContext.resolve(erasedType);
+        CustomDefinition result = new CustomEnumDefinitionProvider(considerJsonValue, considerJsonProperty)
+                .provideCustomSchemaDefinition(type, this.generationContext);
+        Assert.assertNotNull(result);
+        Assert.assertFalse(result.isMeantToBeInline());
+        ObjectNode customDefinitionNode = result.getValue();
+        Assert.assertEquals(2, customDefinitionNode.size());
+        Assert.assertEquals(SchemaKeyword.TAG_TYPE_STRING.forVersion(SchemaVersion.DRAFT_2019_09),
+                customDefinitionNode.get(SchemaKeyword.TAG_TYPE.forVersion(SchemaVersion.DRAFT_2019_09)).asText());
+        JsonNode enumNode = customDefinitionNode.get(SchemaKeyword.TAG_ENUM.forVersion(SchemaVersion.DRAFT_2019_09));
+        Assert.assertTrue(enumNode.isArray());
+        int expectedValueCount = values.size();
+        ArrayNode arrayNode = (ArrayNode) enumNode;
+        Assert.assertEquals(expectedValueCount, arrayNode.size());
+        for (int index = 0; index < expectedValueCount; index++) {
+            Assert.assertEquals(values.get(index), arrayNode.get(index).asText());
+        }
+    }
+
     public Object[] parametersForTestProvideForInvalidTargetType() {
         return new Object[][]{
             {Enum.class, true, false},