Repository: victools/jsonschema-generator
Pull request #48 (2020-03-16 23:41:41)

feat: allow for multiple target type overrides at once
Implements main generator changes for GH-45.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10edcba..a8217af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
 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]
+### Added
+- Support for multiple target type overrides at once per field/method
+
+### Deprecated
+- Configuration option for single target type override
+- Look-up of single target type override from configuration
+
 ## [4.6.0] - 2020-03-15
 ### Added
 - Explicit indication of targeted JSON Schema version (for now: Draft 7 or 2019-09)
@@ -152,6 +160,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Specific handling of optionals (two alternatives via standard options).
 - Pre-defined sets of standard options to cover different use-cases and simplify library usage.
 
+[Unreleased]: https://github.com/victools/jsonschema-generator/compare/v4.6.0...HEAD
 [4.6.0]: https://github.com/victools/jsonschema-generator/compare/v4.5.0...v4.6.0
 [4.5.0]: https://github.com/victools/jsonschema-generator/compare/v4.4.0...v4.5.0
 [4.4.0]: https://github.com/victools/jsonschema-generator/compare/v4.3.0...v4.4.0
diff --git a/README.md b/README.md
index 22fe0d0..05fd397 100644
--- a/README.md
+++ b/README.md
@@ -159,7 +159,7 @@ configBuilder.forTypesInGeneral()
 |    2 | `definitions` | Only for `SchemaVersion.DRAFT_7`: Filled with sub-schemas to support circular references – via `Option.DEFINITIONS_FOR_ALL_OBJECTS` it can be configured whether only sub-schemas appearing more than once are included or all. |
 |    3 | `$defs` | From `SchemaVersion.DRAFT_2019_09`: Filled with sub-schemas to support circular references – via `Option.DEFINITIONS_FOR_ALL_OBJECTS` it can be configured whether only sub-schemas appearing more than once are included or all. |
 |    4 | `$ref` | Used with relative references to sub-schemas in `definitions`. |
-|    5 | `type` | Differentiating between `boolean`/`string`/`integer`/`number` for primitive/known types. `null` is added if a property is deemed nullable according to configuration (`SchemaGeneratorConfigPart.withNullableCheck()`). Arrays and subtypes of `Collection<?>` are treated as `array`, everything else as `object`. A declared type may be interpreted as another type according to configuration (`SchemaGeneratorConfigPart.withTargetTypeOverrideResolver()`). |
+|    5 | `type` | Differentiating between `boolean`/`string`/`integer`/`number` for primitive/known types. `null` is added if a property is deemed nullable according to configuration (`SchemaGeneratorConfigPart.withNullableCheck()`). Arrays and subtypes of `Collection<?>` are treated as `array`, everything else as `object`. A declared type may be interpreted as one or multiple other types according to configuration (`SchemaGeneratorConfigPart.withTargetTypeOverridesResolver()`). |
 |    6 | `properties` | Listing all detected fields and/or methods in an `object`. Which ones are being included can be steered by various `Option`s or via one of the provided `OptionPreset`s as well as by ignoring individual ones via configuration (`SchemaGeneratorConfigPart.withIgnoreCheck()`). Names can be altered via configuration (`SchemaGeneratorConfigPart.withPropertyNameOverrideResolver()`). |
 |    7 | `items` | Indicating the type of `array`/`Collection` elements. |
 |    8 | `required` | Listing the names of fields/methods that are deemed mandatory according to configuration (`SchemaGeneratorConfigPart.withRequiredCheck()`). |
diff --git a/pom.xml b/pom.xml
index 3903344..e4b291f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -85,10 +85,6 @@
         <version.slf4j>1.7.30</version.slf4j>
     </properties>
 
-    <prerequisites>
-        <maven>3.5</maven>
-    </prerequisites>
-
     <dependencies>
         <dependency>
             <groupId>com.fasterxml</groupId>
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
index 773bf44..796ef7a 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
@@ -203,16 +203,42 @@ public interface SchemaGeneratorConfig {
      *
      * @param field object's field/property to determine the target type for
      * @return target type (may be null)
+     * @deprecated use {@link #resolveTargetTypeOverrides(FieldScope)} instead
      */
-    ResolvedType resolveTargetTypeOverride(FieldScope field);
+    @Deprecated
+    default ResolvedType resolveTargetTypeOverride(FieldScope field) {
+        List<ResolvedType> result = this.resolveTargetTypeOverrides(field);
+        return result == null || result.isEmpty() ? null : result.get(0);
+    }
 
     /**
      * Determine the alternative target type from a method's return value.
      *
      * @param method method for whose return value to determine the target type for
      * @return target type (may be null)
+     * @deprecated use {@link #resolveTargetTypeOverrides(MethodScope)} instead
      */
-    ResolvedType resolveTargetTypeOverride(MethodScope method);
+    @Deprecated
+    default ResolvedType resolveTargetTypeOverride(MethodScope method) {
+        List<ResolvedType> result = this.resolveTargetTypeOverrides(method);
+        return result == null || result.isEmpty() ? null : result.get(0);
+    }
+
+    /**
+     * Determine the alternative target types from an object's field/property.
+     *
+     * @param field object's field/property to determine the target type for
+     * @return target types (may be null or empty)
+     */
+    List<ResolvedType> resolveTargetTypeOverrides(FieldScope field);
+
+    /**
+     * Determine the alternative target types from a method's return value.
+     *
+     * @param method method for whose return value to determine the target type for
+     * @return target types (may be null or empty)
+     */
+    List<ResolvedType> resolveTargetTypeOverrides(MethodScope method);
 
     /**
      * Determine the alternative name in a parent JSON Schema's "properties" from an object's field/property.
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
index a852bc9..3b1a389 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
@@ -25,6 +25,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -49,7 +50,7 @@ public class SchemaGeneratorConfigPart<M extends MemberScope<?, ?>> extends Sche
     /*
      * Customising options for the names of properties in a schema with "type": "object".
      */
-    private final List<ConfigFunction<M, ResolvedType>> targetTypeOverrideResolvers = new ArrayList<>();
+    private final List<ConfigFunction<M, List<ResolvedType>>> targetTypeOverridesResolvers = new ArrayList<>();
     private final List<ConfigFunction<M, String>> propertyNameOverrideResolvers = new ArrayList<>();
 
     /**
@@ -140,7 +141,8 @@ public class SchemaGeneratorConfigPart<M extends MemberScope<?, ?>> extends Sche
     }
 
     /**
-     * Setter for target type resolver, expecting the respective member and the default type as inputs.
+     * Setter for target type resolver, expecting the respective member as input. NOTE: there is an alternative method allowing for multiple
+     * type overrides at once: {@link #withTargetTypeOverridesResolver(ConfigFunction)}
      * <br>
      * For generally replacing one type with one or multiple of its subtypes, you may want to consider adding a {@link SubtypeResolver} via
      * {@link SchemaGeneratorConfigBuilder#forTypesInGeneral() forTypesInGeneral()} instead.
@@ -148,9 +150,26 @@ public class SchemaGeneratorConfigPart<M extends MemberScope<?, ?>> extends Sche
      * @param resolver how to determine the alternative target type
      * @return this config part (for chaining)
      * @see SchemaGeneratorGeneralConfigPart#withSubtypeResolver(SubtypeResolver)
+     * @deprecated use {@link #withTargetTypeOverridesResolver(ConfigFunction)} instead
      */
+    @Deprecated
     public SchemaGeneratorConfigPart<M> withTargetTypeOverrideResolver(ConfigFunction<M, ResolvedType> resolver) {
-        this.targetTypeOverrideResolvers.add(resolver);
+        this.targetTypeOverridesResolvers.add(member -> Optional.ofNullable(resolver.apply(member)).map(Collections::singletonList).orElse(null));
+        return this;
+    }
+
+    /**
+     * Setter for target type resolver, expecting the respective member as input. Allowing for multiple alternative types to be returned.
+     * <br>
+     * For generally replacing one type with one or multiple of its subtypes, you may want to consider adding a {@link SubtypeResolver} via
+     * {@link SchemaGeneratorConfigBuilder#forTypesInGeneral() forTypesInGeneral()} instead.
+     *
+     * @param resolver how to determine the alternative target types
+     * @return this config part (for chaining)
+     * @see SchemaGeneratorGeneralConfigPart#withSubtypeResolver(SubtypeResolver)
+     */
+    public SchemaGeneratorConfigPart<M> withTargetTypeOverridesResolver(ConfigFunction<M, List<ResolvedType>> resolver) {
+        this.targetTypeOverridesResolvers.add(resolver);
         return this;
     }
 
@@ -162,8 +181,8 @@ public class SchemaGeneratorConfigPart<M extends MemberScope<?, ?>> extends Sche
      * @see MemberScope#getDeclaredType()
      * @see MemberScope#getOverriddenType()
      */
-    public ResolvedType resolveTargetTypeOverride(M member) {
-        return getFirstDefinedValue(this.targetTypeOverrideResolvers, member);
+    public List<ResolvedType> resolveTargetTypeOverrides(M member) {
+        return getFirstDefinedValue(this.targetTypeOverridesResolvers, member);
     }
 
     /**
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
index 1855764..39bd6ae 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
@@ -471,26 +471,53 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      */
     private void populateField(FieldScope field, Map<String, JsonNode> collectedFields, Set<String> requiredProperties) {
         String propertyNameOverride = this.generatorConfig.resolvePropertyNameOverride(field);
-        FieldScope fieldWithOverride = propertyNameOverride == null ? field : field.withOverriddenName(propertyNameOverride);
-        String propertyName = fieldWithOverride.getSchemaPropertyName();
+        FieldScope fieldWithNameOverride = propertyNameOverride == null ? field : field.withOverriddenName(propertyNameOverride);
+        String propertyName = fieldWithNameOverride.getSchemaPropertyName();
         if (this.generatorConfig.isRequired(field)) {
             requiredProperties.add(propertyName);
         }
         if (collectedFields.containsKey(propertyName)) {
-            logger.debug("ignoring overridden {}.{}", fieldWithOverride.getDeclaringType(), fieldWithOverride.getDeclaredName());
+            logger.debug("ignoring overridden {}.{}", fieldWithNameOverride.getDeclaringType(), fieldWithNameOverride.getDeclaredName());
             return;
         }
-        ObjectNode subSchema = this.generatorConfig.createObjectNode();
-        collectedFields.put(propertyName, subSchema);
-
-        ResolvedType typeOverride = this.generatorConfig.resolveTargetTypeOverride(fieldWithOverride);
-        fieldWithOverride = typeOverride == null ? fieldWithOverride : fieldWithOverride.withOverriddenType(typeOverride);
-
-        ObjectNode fieldAttributes = AttributeCollector.collectFieldAttributes(fieldWithOverride, this);
 
+        List<ResolvedType> typeOverrides = this.generatorConfig.resolveTargetTypeOverrides(fieldWithNameOverride);
+        List<FieldScope> fieldOptions;
+        if (typeOverrides == null || typeOverrides.isEmpty()) {
+            fieldOptions = Collections.singletonList(fieldWithNameOverride);
+        } else {
+            fieldOptions = typeOverrides.stream()
+                    .map(fieldWithNameOverride::withOverriddenType)
+                    .collect(Collectors.toList());
+        }
         // consider declared type (instead of overridden one) for determining null-ability
-        boolean isNullable = !fieldWithOverride.getRawMember().isEnumConstant() && this.generatorConfig.isNullable(fieldWithOverride);
-        this.populateSchema(fieldWithOverride.getType(), subSchema, isNullable, fieldAttributes);
+        boolean isNullable = !field.getRawMember().isEnumConstant() && this.generatorConfig.isNullable(field);
+        if (fieldOptions.size() == 1) {
+            collectedFields.put(propertyName, this.createFieldSchema(fieldOptions.get(0), isNullable));
+        } else {
+            ObjectNode subSchema = this.generatorConfig.createObjectNode();
+            collectedFields.put(propertyName, subSchema);
+            ArrayNode anyOfArray = subSchema.withArray(this.getKeyword(SchemaKeyword.TAG_ANYOF));
+            if (isNullable) {
+                anyOfArray.add(this.generatorConfig.createObjectNode()
+                        .put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_NULL)));
+            }
+            fieldOptions.forEach(option -> anyOfArray.add(this.createFieldSchema(option, false)));
+        }
+    }
+
+    /**
+     * Preparation Step: create a node for a schema representing the given field's associated value type.
+     *
+     * @param field field/property to populate the schema node for
+     * @param isNullable whether the field/property's value may be null
+     * @return schema node representing the given field/property
+     */
+    private ObjectNode createFieldSchema(FieldScope field, boolean isNullable) {
+        ObjectNode subSchema = this.generatorConfig.createObjectNode();
+        ObjectNode fieldAttributes = AttributeCollector.collectFieldAttributes(field, this);
+        this.populateSchema(field.getType(), subSchema, isNullable, fieldAttributes);
+        return subSchema;
     }
 
     /**
@@ -502,31 +529,56 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      */
     private void populateMethod(MethodScope method, Map<String, JsonNode> collectedMethods, Set<String> requiredProperties) {
         String propertyNameOverride = this.generatorConfig.resolvePropertyNameOverride(method);
-        MethodScope methodWithOverride = propertyNameOverride == null ? method : method.withOverriddenName(propertyNameOverride);
-        String propertyName = methodWithOverride.getSchemaPropertyName();
+        MethodScope methodWithNameOverride = propertyNameOverride == null ? method : method.withOverriddenName(propertyNameOverride);
+        String propertyName = methodWithNameOverride.getSchemaPropertyName();
         if (this.generatorConfig.isRequired(method)) {
             requiredProperties.add(propertyName);
         }
         if (collectedMethods.containsKey(propertyName)) {
-            logger.debug("ignoring overridden {}.{}", methodWithOverride.getDeclaringType(), methodWithOverride.getDeclaredName());
+            logger.debug("ignoring overridden {}.{}", methodWithNameOverride.getDeclaringType(), methodWithNameOverride.getDeclaredName());
             return;
         }
 
-        ResolvedType typeOverride = this.generatorConfig.resolveTargetTypeOverride(methodWithOverride);
-        methodWithOverride = typeOverride == null ? methodWithOverride : methodWithOverride.withOverriddenType(typeOverride);
-
-        if (methodWithOverride.isVoid()) {
-            collectedMethods.put(propertyName, BooleanNode.FALSE);
+        List<ResolvedType> typeOverrides = this.generatorConfig.resolveTargetTypeOverrides(methodWithNameOverride);
+        List<MethodScope> methodOptions;
+        if (typeOverrides == null || typeOverrides.isEmpty()) {
+            methodOptions = Collections.singletonList(methodWithNameOverride);
+        } else {
+            methodOptions = typeOverrides.stream()
+                    .map(methodWithNameOverride::withOverriddenType)
+                    .collect(Collectors.toList());
+        }
+        // consider declared type (instead of overridden one) for determining null-ability
+        boolean isNullable = methodWithNameOverride.isVoid() || this.generatorConfig.isNullable(methodWithNameOverride);
+        if (methodOptions.size() == 1) {
+            collectedMethods.put(propertyName, this.createMethodSchema(methodOptions.get(0), isNullable));
         } else {
             ObjectNode subSchema = this.generatorConfig.createObjectNode();
             collectedMethods.put(propertyName, subSchema);
+            ArrayNode anyOfArray = subSchema.withArray(this.getKeyword(SchemaKeyword.TAG_ANYOF));
+            if (isNullable) {
+                anyOfArray.add(this.generatorConfig.createObjectNode()
+                        .put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_NULL)));
+            }
+            methodOptions.forEach(option -> anyOfArray.add(this.createMethodSchema(option, false)));
+        }
+    }
 
-            ObjectNode methodAttributes = AttributeCollector.collectMethodAttributes(methodWithOverride, this);
-
-            // consider declared type (instead of overridden one) for determining null-ability
-            boolean isNullable = this.generatorConfig.isNullable(methodWithOverride);
-            this.populateSchema(methodWithOverride.getType(), subSchema, isNullable, methodAttributes);
+    /**
+     * Preparation Step: create a node for a schema representing the given method's associated return type.
+     *
+     * @param method method to populate the schema node for
+     * @param isNullable whether the method's return value may be null
+     * @return schema node representing the given method's return type
+     */
+    private JsonNode createMethodSchema(MethodScope method, boolean isNullable) {
+        if (method.isVoid()) {
+            return BooleanNode.FALSE;
         }
+        ObjectNode subSchema = this.generatorConfig.createObjectNode();
+        ObjectNode methodAttributes = AttributeCollector.collectMethodAttributes(method, this);
+        this.populateSchema(method.getType(), subSchema, isNullable, methodAttributes);
+        return subSchema;
     }
 
     /**
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
index 8875238..5a397af 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
@@ -215,13 +215,13 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
     }
 
     @Override
-    public ResolvedType resolveTargetTypeOverride(FieldScope field) {
-        return this.fieldConfigPart.resolveTargetTypeOverride(field);
+    public List<ResolvedType> resolveTargetTypeOverrides(FieldScope field) {
+        return this.fieldConfigPart.resolveTargetTypeOverrides(field);
     }
 
     @Override
-    public ResolvedType resolveTargetTypeOverride(MethodScope method) {
-        return this.methodConfigPart.resolveTargetTypeOverride(method);
+    public List<ResolvedType> resolveTargetTypeOverrides(MethodScope method) {
+        return this.methodConfigPart.resolveTargetTypeOverrides(method);
     }
 
     @Override


Test patch:
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
index f464883..f55c913 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
@@ -19,6 +19,7 @@ package com.github.victools.jsonschema.generator;
 import com.fasterxml.classmate.ResolvedType;
 import java.math.BigDecimal;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
 import org.junit.Assert;
@@ -113,25 +114,25 @@ public class SchemaGeneratorConfigPartTest {
         ResolvedType type2 = Mockito.mock(ResolvedType.class);
         ResolvedType type3 = Mockito.mock(ResolvedType.class);
 
-        Assert.assertNull(this.instance.resolveTargetTypeOverride(this.field1));
+        Assert.assertNull(this.instance.resolveTargetTypeOverrides(this.field1));
 
         ConfigFunction<FieldScope, ResolvedType> resolver1 = member -> member == this.field1 ? type1 : null;
         Assert.assertSame(this.instance, this.instance.withTargetTypeOverrideResolver(resolver1));
-        Assert.assertSame(type1, this.instance.resolveTargetTypeOverride(this.field1));
-        Assert.assertNull(this.instance.resolveTargetTypeOverride(this.field2));
-        Assert.assertNull(this.instance.resolveTargetTypeOverride(this.field3));
+        Assert.assertEquals(Collections.singletonList(type1), this.instance.resolveTargetTypeOverrides(this.field1));
+        Assert.assertNull(this.instance.resolveTargetTypeOverrides(this.field2));
+        Assert.assertNull(this.instance.resolveTargetTypeOverrides(this.field3));
 
         ConfigFunction<FieldScope, ResolvedType> resolver2 = member -> member == this.field1 ? type2 : null;
         Assert.assertSame(this.instance, this.instance.withTargetTypeOverrideResolver(resolver2));
-        Assert.assertSame(type1, this.instance.resolveTargetTypeOverride(this.field1));
-        Assert.assertNull(this.instance.resolveTargetTypeOverride(this.field2));
-        Assert.assertNull(this.instance.resolveTargetTypeOverride(this.field3));
-
-        ConfigFunction<FieldScope, ResolvedType> resolver3 = member -> member == this.field2 ? type3 : null;
-        Assert.assertSame(this.instance, this.instance.withTargetTypeOverrideResolver(resolver3));
-        Assert.assertSame(type1, this.instance.resolveTargetTypeOverride(this.field1));
-        Assert.assertSame(type3, this.instance.resolveTargetTypeOverride(this.field2));
-        Assert.assertNull(this.instance.resolveTargetTypeOverride(this.field3));
+        Assert.assertEquals(Collections.singletonList(type1), this.instance.resolveTargetTypeOverrides(this.field1));
+        Assert.assertNull(this.instance.resolveTargetTypeOverrides(this.field2));
+        Assert.assertNull(this.instance.resolveTargetTypeOverrides(this.field3));
+
+        ConfigFunction<FieldScope, List<ResolvedType>> resolver3 = member -> member == this.field2 ? Arrays.asList(type2, type3) : null;
+        Assert.assertSame(this.instance, this.instance.withTargetTypeOverridesResolver(resolver3));
+        Assert.assertEquals(Collections.singletonList(type1), this.instance.resolveTargetTypeOverrides(this.field1));
+        Assert.assertEquals(Arrays.asList(type2, type3), this.instance.resolveTargetTypeOverrides(this.field2));
+        Assert.assertNull(this.instance.resolveTargetTypeOverrides(this.field3));
     }
 
     @Test
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSubtypesTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSubtypesTest.java
index 481b3f2..76807eb 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSubtypesTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSubtypesTest.java
@@ -28,6 +28,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Scanner;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import junitparams.JUnitParamsRunner;
 import junitparams.Parameters;
 import junitparams.naming.TestCaseName;
@@ -63,6 +64,10 @@ public class SchemaGeneratorSubtypesTest {
                 .withSubtypeResolver(new TestSubtypeResolver(subtypes))
                 .withTitleResolver(TypeScope::getSimpleTypeDescription)
                 .withDescriptionResolver(scope -> scope.getType().getErasedType() == TestSuperClass.class ? "supertype-only description" : null);
+        if (!subtypes.isEmpty()) {
+            configBuilder.forFields()
+                    .withTargetTypeOverridesResolver(this::determineTargetTypeOverrides);
+        }
         SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
 
         JsonNode result = generator.generateSchema(TestClassWithSuperTypeReferences.class);
@@ -70,6 +75,17 @@ public class SchemaGeneratorSubtypesTest {
                 loadResource(caseTitle + ".json"), result.toString(), JSONCompareMode.STRICT);
     }
 
+    private List<ResolvedType> determineTargetTypeOverrides(FieldScope field) {
+        ResolvedType declaredType = field.getType();
+        if (declaredType.getErasedType() == Object.class && field.getName().startsWith("numberOrString")) {
+            // TypeContext::resolve() would have been sufficient here, but in advanced scenarios TypeContext::resolveSubType() is more appropriate
+            return Stream.of(Number.class, String.class)
+                    .map(specificSubtype -> field.getContext().resolveSubtype(declaredType, specificSubtype))
+                    .collect(Collectors.toList());
+        }
+        return null;
+    }
+
     private static String loadResource(String resourcePath) throws IOException {
         StringBuilder stringBuilder = new StringBuilder();
         try (InputStream inputStream = SchemaGeneratorSubtypesTest.class
@@ -117,6 +133,7 @@ public class SchemaGeneratorSubtypesTest {
 
         public TestSuperClass<Boolean> booleanSupertypeField;
         public TestSuperClass<String> stringSupertypeField;
+        public Object numberOrStringObjectField;
 
     }
 
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-NO_SUBTYPES.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-NO_SUBTYPES.json
index 0a092f6..59fb0c1 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-NO_SUBTYPES.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-NO_SUBTYPES.json
@@ -27,6 +27,7 @@
         "booleanSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
         },
+        "numberOrStringObjectField": {},
         "stringSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(String)-nullable"
         }
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_INVALID.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_INVALID.json
index 02f2eec..daee406 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_INVALID.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_INVALID.json
@@ -36,6 +36,15 @@
         "booleanSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
         },
+        "numberOrStringObjectField": {
+            "anyOf": [{
+                    "type": "null"
+                }, {
+                    "type": "number"
+                }, {
+                    "type": "string"
+                }]
+        },
         "stringSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(String)-nullable"
         }
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_VALID.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_VALID.json
index 86aa2d7..fb7d0b8 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_VALID.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_VALID.json
@@ -61,6 +61,15 @@
         "booleanSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
         },
+        "numberOrStringObjectField": {
+            "anyOf": [{
+                    "type": "null"
+                }, {
+                    "type": "number"
+                }, {
+                    "type": "string"
+                }]
+        },
         "stringSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(String)-nullable"
         }
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-THREE_SUBTYPES.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-THREE_SUBTYPES.json
index aa17d84..9508acc 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-THREE_SUBTYPES.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-THREE_SUBTYPES.json
@@ -107,6 +107,15 @@
         "booleanSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
         },
+        "numberOrStringObjectField": {
+            "anyOf": [{
+                    "type": "null"
+                }, {
+                    "type": "number"
+                }, {
+                    "type": "string"
+                }]
+        },
         "stringSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(String)-nullable"
         }
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-TWO_SUBTYPES.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-TWO_SUBTYPES.json
index e207089..ce5998f 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-TWO_SUBTYPES.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-TWO_SUBTYPES.json
@@ -61,6 +61,15 @@
         "booleanSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
         },
+        "numberOrStringObjectField": {
+            "anyOf": [{
+                    "type": "null"
+                }, {
+                    "type": "number"
+                }, {
+                    "type": "string"
+                }]
+        },
         "stringSupertypeField": {
             "$ref": "#/definitions/TestSuperClass(String)-nullable"
         }