Repository: victools/jsonschema-generator
Pull request #336 (2023-03-26 21:39:33)

fix: strict type info should be distinct
- adding unit tests to prove current/correct behavior
- add another (unrelated) example


Patch:
diff --git a/jsonschema-examples/src/main/java/com/github/victools/jsonschema/examples/GroupedAnnotationExample.java b/jsonschema-examples/src/main/java/com/github/victools/jsonschema/examples/GroupedAnnotationExample.java
new file mode 100644
index 0000000..d226182
--- /dev/null
+++ b/jsonschema-examples/src/main/java/com/github/victools/jsonschema/examples/GroupedAnnotationExample.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023 VicTools.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.victools.jsonschema.examples;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.FieldScope;
+import com.github.victools.jsonschema.generator.MemberScope;
+import com.github.victools.jsonschema.generator.MethodScope;
+import com.github.victools.jsonschema.generator.OptionPreset;
+import com.github.victools.jsonschema.generator.SchemaGenerator;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import com.github.victools.jsonschema.generator.SchemaVersion;
+import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
+import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Example created in response to <a href="https://github.com/victools/jsonschema-generator/discussions/335">#335</a>.
+ * <br/>
+ * Collecting attributes from annotations grouped via another custom annotation.
+ */
+public class GroupedAnnotationExample implements SchemaGenerationExampleInterface {
+
+    @Override
+    public ObjectNode generateSchema() {
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON)
+                .with(new GroupedAnnotationAwareJakartaValidationModule(JakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS))
+                .build();
+        SchemaGenerator generator = new SchemaGenerator(config);
+        return generator.generateSchema(Example.class);
+    }
+
+    /**
+     * Extend standard Jakarta validation module (exactly the same can be done for the Javax validation module), in order to utilise existing logic.
+     */
+    static class GroupedAnnotationAwareJakartaValidationModule extends JakartaValidationModule {
+
+        GroupedAnnotationAwareJakartaValidationModule(JakartaValidationOption... options) {
+            super(options);
+        }
+
+        @Override
+        protected <A extends Annotation> A getAnnotationFromFieldOrGetter(MemberScope<?, ?> member, Class<A> annotationClass,
+                Function<A, Class<?>[]> validationGroupsLookup) {
+            A annotation = super.getAnnotationFromFieldOrGetter(member, annotationClass, validationGroupsLookup);
+            if (annotation != null) {
+                return annotation;
+            }
+            if (!member.isFakeContainerItemScope()) {
+                Optional<A> groupedAnnotationFromMember = this.getGroupedAnnotationFromMember(member, annotationClass);
+                if (!groupedAnnotationFromMember.isPresent()) {
+                    MemberScope<?, ?> alternativeMember;
+                    if (member instanceof FieldScope) {
+                        alternativeMember = ((FieldScope) member).findGetter();
+                    } else if (member instanceof MethodScope) {
+                        alternativeMember = ((MethodScope) member).findGetterField();
+                    } else {
+                        return null;
+                    }
+                    if (alternativeMember != null) {
+                        groupedAnnotationFromMember = this.getGroupedAnnotationFromMember(alternativeMember, annotationClass);
+                    }
+                }
+                return groupedAnnotationFromMember.orElse(null);
+            }
+            return null;
+        }
+
+        private <A extends Annotation> Optional<A> getGroupedAnnotationFromMember(MemberScope<?, ?> member, Class<A> annotationClass) {
+            for (Annotation otherAnnotation : member.getMember().getAnnotations()) {
+                A annotation = otherAnnotation.annotationType().getDeclaredAnnotation(annotationClass);
+                if (annotation != null) {
+                    return Optional.of(annotation);
+                }
+            }
+            return Optional.empty();
+        }
+    }
+
+    static class Example {
+        @CustomIdentifier
+        private String identifier;
+    }
+
+    @Pattern(regexp = "^[a-zA-Z#@$][a-zA-Z#@$0-9]*$")
+    @Size(min = 1, max = 8)
+    @Constraint(validatedBy = {})
+    @Target({ElementType.PARAMETER, ElementType.FIELD})
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface CustomIdentifier {
+
+        String message() default "";
+
+        Class<?>[] groups() default {};
+
+        Class<? extends Payload>[] payload() default {};
+    }
+}
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java
index c083985..a31c131 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java
@@ -490,6 +490,7 @@ public class SchemaCleanUpUtils {
         List<String> impliedTypes = reverseTagMap.entrySet().stream()
                 .filter(entry -> schemaNode.has(entry.getKey()))
                 .flatMap(entry -> entry.getValue().getImpliedTypes().stream())
+                .distinct()
                 .sorted()
                 .map(SchemaKeyword.SchemaType::getSchemaKeywordValue)
                 .collect(Collectors.toList());


Test patch:
diff --git a/jsonschema-examples/src/test/java/com/github/victools/jsonschema/examples/ExampleTest.java b/jsonschema-examples/src/test/java/com/github/victools/jsonschema/examples/ExampleTest.java
index 01ba50b..551c934 100644
--- a/jsonschema-examples/src/test/java/com/github/victools/jsonschema/examples/ExampleTest.java
+++ b/jsonschema-examples/src/test/java/com/github/victools/jsonschema/examples/ExampleTest.java
@@ -33,6 +33,7 @@ public class ExampleTest {
     @ValueSource(classes = {
             ExternalRefAnnotationExample.class,
             ExternalRefPackageExample.class,
+            GroupedAnnotationExample.class,
             IfThenElseExample.class,
             StrictTypeInfoExample.class
     })
diff --git a/jsonschema-examples/src/test/resources/com/github/victools/jsonschema/examples/GroupedAnnotationExample-result.json b/jsonschema-examples/src/test/resources/com/github/victools/jsonschema/examples/GroupedAnnotationExample-result.json
new file mode 100644
index 0000000..86dc8c5
--- /dev/null
+++ b/jsonschema-examples/src/test/resources/com/github/victools/jsonschema/examples/GroupedAnnotationExample-result.json
@@ -0,0 +1,12 @@
+{
+  "$schema" : "https://json-schema.org/draft/2019-09/schema",
+  "type" : "object",
+  "properties" : {
+    "identifier" : {
+      "type" : "string",
+      "minLength" : 1,
+      "maxLength" : 8,
+      "pattern" : "^[a-zA-Z#@$][a-zA-Z#@$0-9]*$"
+    }
+  }
+}
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
index 7423ccf..ed4464d 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
@@ -93,6 +93,7 @@ public class SchemaGeneratorComplexTypesTest {
         configPart
                 .withNullableCheck(member -> !member.getName().startsWith("nested"))
                 .withRequiredCheck(member -> member.getName().startsWith("nested"))
+                .withDependentRequiresResolver(member -> "optionalS".equals(member.getName()) ? Collections.singletonList("listOfOptionalS") : null)
                 .withReadOnlyCheck(MemberScope::isFinal)
                 .withWriteOnlyCheck(member -> member.getType() != null && TestClass4.class.isAssignableFrom(member.getType().getErasedType()));
     }
@@ -142,7 +143,7 @@ public class SchemaGeneratorComplexTypesTest {
         final SchemaVersion schemaVersion = SchemaVersion.DRAFT_7;
         SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(schemaVersion, preset);
         configBuilder.with(testModule);
-        configBuilder.with(Option.NULLABLE_ARRAY_ITEMS_ALLOWED);
+        configBuilder.with(Option.NULLABLE_ARRAY_ITEMS_ALLOWED, Option.STRICT_TYPE_INFO);
         SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
 
         JsonNode result = generator.generateSchema(targetType);
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtilsTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtilsTest.java
new file mode 100644
index 0000000..5d7797d
--- /dev/null
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtilsTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 VicTools.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.victools.jsonschema.generator.impl;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.OptionPreset;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
+import com.github.victools.jsonschema.generator.SchemaVersion;
+import java.util.Collections;
+import java.util.stream.Stream;
+import org.json.JSONArray;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mock;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class SchemaCleanUpUtilsTest {
+
+    Stream<Arguments> parametersForTestSetStrictTypeInfo() {
+        return Stream.of(
+                Arguments.of("{ \"title\": \"test\" }", true,
+                        "{ \"title\": \"test\" }"),
+                Arguments.of("{ \"description\": \"test\" }", true,
+                        "{ \"description\": \"test\" }"),
+                Arguments.of("{ \"$id\": \"test\" }", true,
+                        "{ \"$id\": \"test\" }"),
+                Arguments.of("{ \"properties\": { \"test\": {} } }", true,
+                        "{ \"properties\": { \"test\": {} }, \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{ \"type\": \"object\", \"properties\": { \"test\": {} } }", true,
+                        "{ \"type\": \"object\", \"properties\": { \"test\": {} } }"),
+                Arguments.of("{ \"required\": [] }", true,
+                        "{ \"required\": [], \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{ \"properties\": { \"test\": {} }, \"required\": [] }", true,
+                        "{ \"properties\": { \"test\": {} }, \"required\": [], \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{ \"items\": {} }", true,
+                        "{ \"items\": {}, \"type\": [\"array\", \"null\"] }"),
+                Arguments.of("{ \"properties\": { \"test\": {} }, \"items\": {} }", true,
+                        "{ \"properties\": { \"test\": {} }, \"items\": {}, \"type\": [\"array\", \"object\", \"null\"] }"),
+                Arguments.of("{ \"unevaluatedItems\": {} }", true,
+                        "{ \"unevaluatedItems\": {}, \"type\": [\"array\", \"null\"] }"),
+                Arguments.of("{ \"prefixItems\": [] }", true,
+                        "{ \"prefixItems\": [], \"type\": [\"array\", \"null\"] }"),
+                Arguments.of("{ \"dependentSchemas\": { \"test\": {} } }", true,
+                        "{ \"dependentSchemas\": { \"test\": {} }, \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{ \"dependentRequired\": { \"test\": [] } }", true,
+                        "{ \"dependentRequired\": { \"test\": [] }, \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{ \"additionalProperties\": {} }", true,
+                        "{ \"additionalProperties\": {}, \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{ \"patternProperties\": { \"a[bc]\": {} } }", true,
+                        "{ \"patternProperties\": { \"a[bc]\": {} }, \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{ \"minProperties\": 1 }", true,
+                        "{ \"minProperties\": 1, \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{ \"maxProperties\": 3 }", true,
+                        "{ \"maxProperties\": 3, \"type\": [\"object\", \"null\"] }"),
+                Arguments.of("{\"allOf\":[{\"anyOf\":[{\"oneOf\":[{\"not\":{\"required\":[\"test\"],\"type\":[\"object\",\"null\"]}}]}]}]}", true,
+                        "{\"allOf\":[{\"anyOf\":[{\"oneOf\":[{\"not\":{\"required\":[\"test\"],\"type\":[\"object\",\"null\"]}}]}]}]}"),
+                Arguments.of("{ \"minLength\": 1 }", true,
+                        "{ \"minLength\": 1, \"type\": [\"string\", \"null\"] }"),
+                Arguments.of("{ \"maxLength\": 1 }", true,
+                        "{ \"maxLength\": 1, \"type\": [\"string\", \"null\"] }"),
+                Arguments.of("{ \"format\": \"email\" }", true,
+                        "{ \"format\": \"email\", \"type\": [\"string\", \"null\"] }"),
+                Arguments.of("{ \"pattern\": \"a[bc]\" }", true,
+                        "{ \"pattern\": \"a[bc]\", \"type\": [\"string\", \"null\"] }"),
+                Arguments.of("{ \"minimum\": 10 }", true,
+                        "{ \"minimum\": 10, \"type\": [\"integer\", \"number\", \"null\"] }"),
+                Arguments.of("{ \"maximum\": 100 }", true,
+                        "{ \"maximum\": 100, \"type\": [\"integer\", \"number\", \"null\"] }"),
+                Arguments.of("{ \"exclusiveMinimum\": 9 }", true,
+                        "{ \"exclusiveMinimum\": 9, \"type\": [\"integer\", \"number\", \"null\"] }"),
+                Arguments.of("{ \"exclusiveMaximum\": 101 }", true,
+                        "{ \"exclusiveMaximum\": 101, \"type\": [\"integer\", \"number\", \"null\"] }"),
+                Arguments.of("{ \"minimum\": 10, \"maximum\": 100 }", true,
+                        "{ \"minimum\": 10, \"maximum\": 100, \"type\": [\"integer\", \"number\", \"null\"] }"),
+                Arguments.of("{ \"multipleOf\": 2 }", true,
+                        "{ \"multipleOf\": 2, \"type\": [\"integer\", \"number\", \"null\"] }"),
+                Arguments.of("{ \"if\": {}, \"then\": {}, \"else\": {} }", true,
+                        "{ \"if\": {}, \"then\": {}, \"else\": {} }")
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("parametersForTestSetStrictTypeInfo")
+    public void testSetStrictTypeInfo(String schemaInput, boolean considerNullType, String expectedOutput) throws Exception {
+        SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON);
+        SchemaCleanUpUtils utilsInstance = new SchemaCleanUpUtils(configBuilder.build());
+
+        JsonNode schema = configBuilder.getObjectMapper().readTree(schemaInput);
+        utilsInstance.setStrictTypeInfo(Collections.singletonList((ObjectNode) schema), considerNullType);
+
+        String schemaAsString = schema.toString();
+        JSONAssert.assertEquals('\n' + schemaAsString +'\n', expectedOutput, schemaAsString, JSONCompareMode.STRICT);
+    }
+}
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-methodattributes.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-methodattributes.json
index 62e2960..2543047 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-methodattributes.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-methodattributes.json
@@ -54,20 +54,24 @@
                     "pattern": "^.{1,256}$"
                 },
                 "valueOf(String)": {
-                    "allOf": [{
+                    "allOf": [
+                        {
                             "$ref": "#/definitions/RoundingMode-nullable"
                         }, {
                             "title": "RoundingMode",
                             "description": "looked-up from method: RoundingMode"
-                        }]
+                        }
+                    ]
                 },
                 "valueOf(int)": {
-                    "allOf": [{
+                    "allOf": [
+                        {
                             "$ref": "#/definitions/RoundingMode-nullable"
                         }, {
                             "title": "RoundingMode",
                             "description": "looked-up from method: RoundingMode"
-                        }]
+                        }
+                    ]
                 },
                 "values()": {
                     "title": "RoundingMode[]",
@@ -77,22 +81,26 @@
                     "uniqueItems": false,
                     "type": ["array", "null"],
                     "items": {
-                        "allOf": [{
+                        "allOf": [
+                            {
                                 "$ref": "#/definitions/RoundingMode-nullable"
                             }, {
                                 "title": "RoundingMode",
                                 "description": "looked-up from method: RoundingMode"
-                            }]
+                            }
+                        ]
                     }
                 }
             }
         },
         "RoundingMode-nullable": {
-            "anyOf": [{
+            "anyOf": [
+                {
                     "type": "null"
                 }, {
                     "$ref": "#/definitions/RoundingMode"
-                }]
+                }
+            ]
         },
         "TestClass1": {
             "type": "object",
@@ -181,11 +189,13 @@
             "type": ["object", "null"],
             "properties": {
                 "DEFAULT_ROUNDING_MODE": {
-                    "allOf": [{
+                    "allOf": [
+                        {
                             "$ref": "#/definitions/RoundingMode"
                         }, {
                             "const": "HALF_UP"
-                        }]
+                        }
+                    ]
                 },
                 "class2OfClass2OfT": {
                     "type": ["object", "null"],
@@ -197,11 +207,13 @@
                             }
                         },
                         "genericValue": {
-                            "anyOf": [{
+                            "anyOf": [
+                                {
                                     "type": "null"
                                 }, {
                                     "$ref": "#/definitions/TestClass2(String)"
-                                }],
+                                }
+                            ],
                             "title": "TestClass2<String>",
                             "description": "looked-up from method: TestClass2<String>",
                             "additionalProperties": false,
@@ -209,7 +221,8 @@
                                 "^generic.+$": {
                                     "type": "string"
                                 }
-                            }
+                            },
+                            "type": ["object", "null"]
                         }
                     },
                     "title": "TestClass2<TestClass2<String>>",
@@ -293,11 +306,13 @@
                     "uniqueItems": false,
                     "type": ["array", "null"],
                     "items": {
-                        "anyOf": [{
+                        "anyOf": [
+                            {
                                 "type": "null"
                             }, {
                                 "$ref": "#/definitions/TestClass1"
-                            }],
+                            }
+                        ],
                         "title": "TestClass1",
                         "description": "looked-up from method: TestClass1",
                         "additionalProperties": false,
@@ -305,7 +320,8 @@
                             "^generic.+$": {
                                 "type": "string"
                             }
-                        }
+                        },
+                        "type": ["object", "null"]
                     }
                 }
             },
@@ -322,7 +338,8 @@
             }
         },
         "nestedLong": {
-            "allOf": [{
+            "allOf": [
+                {
                     "$ref": "#/definitions/TestClass2(Long)-nullable"
                 }, {
                     "title": "TestClass2<Long>",
@@ -332,8 +349,10 @@
                         "^generic.+$": {
                             "type": "integer"
                         }
-                    }
-                }]
+                    },
+                    "type": ["object", "null"]
+                }
+            ]
         },
         "nestedLongList": {
             "title": "List<TestClass2<Long>>",
@@ -343,7 +362,8 @@
             "uniqueItems": false,
             "type": ["array", "null"],
             "items": {
-                "allOf": [{
+                "allOf": [
+                    {
                         "$ref": "#/definitions/TestClass2(Long)-nullable"
                     }, {
                         "title": "TestClass2<Long>",
@@ -353,8 +373,10 @@
                             "^generic.+$": {
                                 "type": "integer"
                             }
-                        }
-                    }]
+                        },
+                        "type": ["object", "null"]
+                    }
+                ]
             }
         }
     }
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
index b861440..747cc67 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
@@ -201,6 +201,9 @@
                     "multipleOf": 1
                 }
             },
+            "dependencies": {
+                "optionalS": ["listOfOptionalS"]
+            },
             "title": "TestClass4<Integer, String>",
             "description": "looked-up from field: TestClass4<Integer, String>",
             "writeOnly": true,