Repository: victools/jsonschema-generator (java)

Issue:
Map additionalProperties sometimes missing with interface value
Hi,

I have an issue with Maps wit Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES, when the Map Value type is 
not a concrete class. In my case we use a custom subTypeResolver picking the concrete implementation from @JsonDeserialize or @JsonSerialize annotations. Using the concrete Impl (Map<String, BImpl>) works as expected.
Is there something I miss, or should the AdditionalPropertiesModule use the subTypeResolver as expected?

```java
@JsonSerialize(as = AImpl.class)
public interface A {
    Map<String, B> getMap(); 
}

@JsonSerialize(as = BImpl.class)
public interface B {
    String getName();
}
```

**Outcome**:
```json
"map" : {
  "type" : "object",
  "additionalProperties" : { }
},
``` 
**Expected**:
```json
"map" : {
  "type" : "object",
  "additionalProperties" : { 
     "name": { "type": "string"}
   }
}
```


Pull request description:
fix: subtype resolution with single result
Although this was reported in the context of `additionalProperties` it is actually a general fix for the combination of the following conditions:
- A configured `SubtypeResolver` (standard/built-in or custom) returns a single subtype for some given supertype.
- The default logic for "schema inlining" is enabled (i.e., neither `Option.INLINE_ALL_SCHEMAS` nor ` Option.DEFINITIONS_FOR_ALL_OBJECTS` are  used)
- The supertype is only referenced once, i.e., gets replaced with its inline schema definition at the end of the generation process.

Closes #261.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75bd799..de8e90f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - new `Option.FLATTENED_SUPPLIERS` to unwrap the supplied type; `Supplier<T>` would thus be a type `T`
 
 #### Fixed
+- when resolving subtypes with a single other type, under some circumstances the type definition gets lost
 - set default `ObjectMapper` node factory to `JsonNodeFactory.withExactBigDecimals(true)` to avoid scientific notation for numbers
 
 #### Changed
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
index aa71672..dbd37e3 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
@@ -332,14 +332,10 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             this.generateObjectDefinition(targetType, definition);
             return false;
         }
-        if (subtypes.size() == 1) {
-            // avoid unnecessary "anyOf" by making the definition a direct reference to the subtype's definition
-            this.traverseGenericType(subtypes.get(0), definition, false);
-        } else {
-            ArrayNode anyOfArrayNode = this.generatorConfig.createArrayNode();
-            subtypes.forEach(subtype -> this.traverseGenericType(subtype, anyOfArrayNode.addObject(), false));
-            definition.set(this.getKeyword(SchemaKeyword.TAG_ANYOF), anyOfArrayNode);
-        }
+        // always wrap subtype definitions, in order to avoid pointing at the same definition node as the super type
+        SchemaKeyword arrayNodeName = subtypes.size() == 1 ? SchemaKeyword.TAG_ALLOF : SchemaKeyword.TAG_ANYOF;
+        ArrayNode subtypeDefinitionArrayNode = definition.withArray(this.getKeyword(arrayNodeName));
+        subtypes.forEach(subtype -> this.traverseGenericType(subtype, subtypeDefinitionArrayNode.addObject(), false));
         return true;
     }
 


Test patch:
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/module/AdditionalPropertiesModuleTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/module/AdditionalPropertiesModuleTest.java
index 45223de..9cf7491 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/module/AdditionalPropertiesModuleTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/module/AdditionalPropertiesModuleTest.java
@@ -16,19 +16,27 @@
 
 package com.github.victools.jsonschema.generator.impl.module;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.github.victools.jsonschema.generator.AbstractTypeAwareTest;
 import com.github.victools.jsonschema.generator.ConfigFunction;
 import com.github.victools.jsonschema.generator.Module;
+import com.github.victools.jsonschema.generator.Option;
+import com.github.victools.jsonschema.generator.OptionPreset;
+import com.github.victools.jsonschema.generator.SchemaGenerator;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
 import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart;
 import com.github.victools.jsonschema.generator.SchemaVersion;
+import com.github.victools.jsonschema.generator.TestUtils;
 import com.github.victools.jsonschema.generator.TypeContext;
 import com.github.victools.jsonschema.generator.TypeScope;
+import java.io.IOException;
 import java.lang.reflect.Type;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
+import org.json.JSONException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -37,6 +45,8 @@ import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
 
 /**
  * Test for the {@link AdditionalPropertiesModule} class.
@@ -115,7 +125,46 @@ public class AdditionalPropertiesModuleTest extends AbstractTypeAwareTest {
         }
     }
 
+    @Test
+    public void testAdditionalPropertyWithSubtype() throws JSONException, IOException {
+        SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON);
+        configBuilder.with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES)
+                .without(Option.SCHEMA_VERSION_INDICATOR)
+                .forTypesInGeneral()
+                .withSubtypeResolver((declaredType, context) -> declaredType.getErasedType() == B.class
+                        ? Collections.singletonList(context.getTypeContext().resolve(BImpl.class))
+                        : null);
+        SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
+        JsonNode result = generator.generateSchema(A.class);
+        JSONAssert.assertEquals(TestUtils.loadResource(AdditionalPropertiesModuleTest.class, "additional-property-with-subtype.json"),
+                result.toString(), JSONCompareMode.STRICT);
+    }
+
     private static class TestMapSubType extends HashMap<Object, String> {
         // no further fields
     }
+
+    private static class A {
+
+        private Map<String, B> map;
+
+        Map<String, B> getMap() {
+            return this.map;
+        }
+    }
+
+    private interface B {
+
+        String getName();
+    }
+
+    private static class BImpl implements B {
+
+        private String name;
+
+        @Override
+        public String getName() {
+            return this.name;
+        }
+    }
 }
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/impl/module/additional-property-with-subtype.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/impl/module/additional-property-with-subtype.json
new file mode 100644
index 0000000..77bc3f8
--- /dev/null
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/impl/module/additional-property-with-subtype.json
@@ -0,0 +1,16 @@
+{
+  "type" : "object",
+  "properties" : {
+    "map" : {
+      "type" : "object",
+      "additionalProperties" : {
+        "type" : "object",
+        "properties" : {
+          "name" : {
+            "type" : "string"
+          }
+        }
+      }
+    }
+  }
+}