Repository: victools/jsonschema-generator
Pull request #414 (2023-11-19 22:23:18)

chore: further code health refactoring


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8c57ab..155e0d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 #### Changed
 - include new `Option.STANDARD_FORMATS` in `OptionPreset.PLAIN_JSON` by default
 
+#### Fixed
+- when using `Option.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS` on a method where the second character of the derived field name is in uppercase, don't capitalise the first character
+
 ## [4.32.0] - 2023-10-27
 ### `jsonschema-generator`
 #### Added
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/FieldScope.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/FieldScope.java
index 4ac79dc..ff5aa88 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/FieldScope.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/FieldScope.java
@@ -126,7 +126,8 @@ public class FieldScope extends MemberScope<ResolvedField, Field> {
         possibleGetterNames.add("get" + capitalisedFieldName);
         possibleGetterNames.add("is" + capitalisedFieldName);
         // @since 4.32.0 - for a field like "isBool" also consider "isBool()" as potential getter method
-        if (declaredName.startsWith("is") && declaredName.length() > 2 && Character.isUpperCase(declaredName.charAt(2))) {
+        boolean fieldNameStartsWithIs = declaredName.startsWith("is") && declaredName.length() > 2 && Character.isUpperCase(declaredName.charAt(2));
+        if (fieldNameStartsWithIs) {
             possibleGetterNames.add(declaredName);
         }
         ResolvedMethod[] methods = this.getDeclaringTypeMembers().getMemberMethods();
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MemberScope.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MemberScope.java
index f4473b6..77b80a0 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MemberScope.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MemberScope.java
@@ -445,9 +445,11 @@ public abstract class MemberScope<M extends ResolvedMember<T>, T extends Member>
         if (this.isFakeContainerItemScope()) {
             return this.getContainerItemAnnotationConsideringFieldAndGetter(annotationClass, considerOtherAnnotation);
         }
-        if (this.getOverriddenType() != null
-                && this.getDeclaredType().getErasedType() == Optional.class
-                && this.getOverriddenType().getErasedType() == this.getDeclaredType().getTypeParameters().get(0).getErasedType()) {
+        // in addition to an explicitly marked "faked container item scope", also support a type wrapped in an Optional
+        if (this.getOverriddenType() == null || this.getDeclaredType().getErasedType() != Optional.class) {
+            return null;
+        }
+        if (this.getOverriddenType().getErasedType() == this.getDeclaredType().getTypeParameters().get(0).getErasedType()) {
             return this.getContainerItemAnnotationConsideringFieldAndGetter(annotationClass, considerOtherAnnotation);
         }
         return null;
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java
index 1a027a8..434f602 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java
@@ -24,8 +24,8 @@ import java.lang.annotation.Annotation;
 import java.lang.reflect.AnnotatedType;
 import java.lang.reflect.Method;
 import java.util.Arrays;
-import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -126,17 +126,18 @@ public class MethodScope extends MemberScope<ResolvedMethod, Method> {
      * @return associated field
      */
     private FieldScope doFindGetterField() {
-        if (this.getType() == null || !this.isPublic() || this.getArgumentCount() > 0) {
-            // void and non-public methods or those with arguments are not deemed to be getters
+        if (this.getType() == null || this.getArgumentCount() > 0) {
+            // void methods or those with arguments are not deemed to be getters
             return null;
         }
-        String methodName = this.getDeclaredName();
-        Set<String> possibleFieldNames = new HashSet<>(3);
-        if (methodName.startsWith("get")) {
-            getPossibleFieldNamesStartingWithGet(methodName, possibleFieldNames);
-        } else if (methodName.startsWith("is")) {
-            getPossibleFieldNamesStartingWithIs(methodName, possibleFieldNames);
+        if (!this.isPublic()) {
+            // only public methods are deemed eligible to be getters
+            return null;
         }
+        String methodName = this.getDeclaredName();
+        Set<String> possibleFieldNames = Stream.of("get", "is")
+                .flatMap(prefix -> getPossibleFieldNames(prefix, methodName))
+                .collect(Collectors.toSet());
         if (possibleFieldNames.isEmpty()) {
             // method name does not fall into getter conventions
             return null;
@@ -150,28 +151,25 @@ public class MethodScope extends MemberScope<ResolvedMethod, Method> {
                 .orElse(null);
     }
 
-    private static void getPossibleFieldNamesStartingWithGet(String methodName, Set<String> possibleFieldNames) {
-        if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) {
-            // ensure that the variable starts with a lower-case letter
-            possibleFieldNames.add(methodName.substring(3, 4).toLowerCase() + methodName.substring(4));
-        }
-        // @since 4.32.0 - conforming with JavaBeans API specification edge case when second character in field name is in uppercase
-        if (methodName.length() > 4 && Character.isUpperCase(methodName.charAt(4))) {
-            possibleFieldNames.add(methodName.substring(3));
+    private static Stream<String> getPossibleFieldNames(String prefix, String methodName) {
+        if (!methodName.startsWith(prefix) || prefix.length() == methodName.length()) {
+            return Stream.of();
         }
-    }
-
-    private static void getPossibleFieldNamesStartingWithIs(String methodName, Set<String> possibleFieldNames) {
-        if (methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) {
+        Stream.Builder<String> possibleFieldNames = Stream.builder();
+        String methodNameWithoutPrefix = methodName.substring(prefix.length());
+        if (Character.isUpperCase(methodNameWithoutPrefix.charAt(0))) {
             // ensure that the variable starts with a lower-case letter
-            possibleFieldNames.add(methodName.substring(2, 3).toLowerCase() + methodName.substring(3));
-            // since 4.32.0: a method "isBool()" is considered a possible getter for a field "isBool" as well as for "bool"
-            possibleFieldNames.add(methodName);
+            possibleFieldNames.add(methodNameWithoutPrefix.substring(0, 1).toLowerCase() + methodNameWithoutPrefix.substring(1));
+            if ("is".equals(prefix)) {
+                // since 4.32.0: a method "isBool()" is considered a possible getter for a field "isBool" as well as for "bool"
+                possibleFieldNames.add(methodName);
+            }
         }
         // @since 4.32.0 - conforming with JavaBeans API specification edge case when second character in field name is in uppercase
-        if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) {
-            possibleFieldNames.add(methodName.substring(2));
+        if (methodNameWithoutPrefix.length() > 1 && Character.isUpperCase(methodNameWithoutPrefix.charAt(1))) {
+            possibleFieldNames.add(methodNameWithoutPrefix);
         }
+        return possibleFieldNames.build();
     }
 
     /**
@@ -213,7 +211,9 @@ public class MethodScope extends MemberScope<ResolvedMethod, Method> {
         A annotation = this.getAnnotation(annotationClass, considerOtherAnnotation);
         if (annotation == null) {
             MemberScope<?, ?> associatedField = this.findGetterField();
-            annotation = associatedField == null ? null : associatedField.getAnnotation(annotationClass, considerOtherAnnotation);
+            if (associatedField != null) {
+                annotation = associatedField.getAnnotation(annotationClass, considerOtherAnnotation);
+            }
         }
         return annotation;
     }
@@ -224,7 +224,9 @@ public class MethodScope extends MemberScope<ResolvedMethod, Method> {
         A annotation = this.getContainerItemAnnotation(annotationClass, considerOtherAnnotation);
         if (annotation == null) {
             MemberScope<?, ?> associatedField = this.findGetterField();
-            annotation = associatedField == null ? null : associatedField.getContainerItemAnnotation(annotationClass, considerOtherAnnotation);
+            if (associatedField != null) {
+                annotation = associatedField.getContainerItemAnnotation(annotationClass, considerOtherAnnotation);
+            }
         }
         return annotation;
     }
@@ -238,23 +240,36 @@ public class MethodScope extends MemberScope<ResolvedMethod, Method> {
      */
     @Override
     protected String doGetSchemaPropertyName() {
-        String result = this.getName();
-        if (this.getContext().isDerivingFieldsFromArgumentFreeMethods() && this.getArgumentCount() == 0) {
-            if (this.getOverriddenName() == null) {
-                // remove the "get"/"is" prefix from non-overridden method names
-                if (result.startsWith("get") && result.length() > 3) {
-                    result = Character.toLowerCase(result.charAt(3)) + result.substring(4);
-                } else if (result.startsWith("is") && result.length() > 2) {
-                    result = Character.toLowerCase(result.charAt(2)) + result.substring(3);
-                } else {
-                    result += "()";
-                }
-            }
-        } else {
-            result += this.getArgumentTypes().stream()
+        String result;
+        if (!this.getContext().isDerivingFieldsFromArgumentFreeMethods() || this.getArgumentCount() > 0) {
+            result = this.getName() + this.getArgumentTypes().stream()
                     .map(this.getContext()::getMethodPropertyArgumentTypeDescription)
                     .collect(Collectors.joining(", ", "(", ")"));
+        } else if (this.getOverriddenName() == null) {
+            result = this.deriveFieldName();
+        } else {
+            result = this.getName();
         }
         return result;
     }
+
+    private String deriveFieldName() {
+        String methodName = this.getDeclaredName();
+        // remove the "get"/"is" prefix from non-overridden method names
+        Optional<String> possibleFieldName = Stream.of("get", "is")
+                .filter(methodName::startsWith)
+                .map(prefix -> methodName.substring(prefix.length()))
+                .filter(name -> !name.isEmpty())
+                .findFirst();
+        if (!possibleFieldName.isPresent()) {
+            return methodName + "()";
+        }
+        String methodNameWithoutPrefix = possibleFieldName.get();
+        if (Character.isUpperCase(methodNameWithoutPrefix.charAt(1))) {
+            // since 4.33.0 according to the Java Beans specification, if the second character is uppercase the first should remain as-is
+            // e.g., getxIndex() corresponds to "xIndex" and isURL() to "URL"
+            return methodNameWithoutPrefix;
+        }
+        return Character.toLowerCase(methodNameWithoutPrefix.charAt(0)) + methodNameWithoutPrefix.substring(1);
+    }
 }
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
index d9ab658..291d66e 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
@@ -32,6 +32,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.WeakHashMap;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -188,10 +189,15 @@ public class TypeContext {
      */
     public ResolvedType getTypeParameterFor(ResolvedType type, Class<?> erasedSuperType, int parameterIndex) {
         List<ResolvedType> typeParameters = type.typeParametersFor(erasedSuperType);
-        if (typeParameters == null
-                || (!typeParameters.isEmpty() && typeParameters.size() <= parameterIndex)
-                || (typeParameters.isEmpty() && erasedSuperType.getTypeParameters().length <= parameterIndex)) {
-            // given type is not a super type of the type in scope or given index is out of bounds
+        if (typeParameters == null) {
+            // given type is not a super type of the type in scope
+            return null;
+        }
+        if (!typeParameters.isEmpty() && typeParameters.size() <= parameterIndex) {
+            // given index is out of bounds for the specific type
+        }
+        if (typeParameters.isEmpty() && erasedSuperType.getTypeParameters().length <= parameterIndex) {
+            // given index is out of bounds for the designated super type
             return null;
         }
         if (typeParameters.isEmpty()) {
@@ -441,4 +447,27 @@ public class TypeContext {
     public String getMethodPropertyArgumentTypeDescription(ResolvedType type) {
         return this.getSimpleTypeDescription(type);
     }
+
+    /**
+     * Helper function to write generic code that targets either a {@link FieldScope} or {@link MethodScope}.
+     *
+     * @param <R> type of expected return value
+     * @param member field or method being targeted
+     * @param fieldAction action to perform in case the given member is a {@link FieldScope}
+     * @param methodAction action to perform in case the given member is a {@link MethodScope}
+     * @return value returned by the performed action
+     * @throws IllegalStateException if given member is neither a {@link FieldScope} or {@link MethodScope}
+     */
+    public <R> R performActionOnMember(MemberScope<?, ?> member, Function<FieldScope, R> fieldAction,
+            Function<MethodScope, R> methodAction) {
+        R result;
+        if (member instanceof FieldScope) {
+            result = fieldAction.apply((FieldScope) member);
+        } else if (member instanceof MethodScope) {
+            result = methodAction.apply((MethodScope) member);
+        } else {
+            throw new IllegalStateException("Unsupported member scope of type: " + member.getClass());
+        }
+        return result;
+    }
 }
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/MemberCollectionContextImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/MemberCollectionContextImpl.java
new file mode 100644
index 0000000..af40433
--- /dev/null
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/MemberCollectionContextImpl.java
@@ -0,0 +1,179 @@
+/*
+ * 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.classmate.ResolvedType;
+import com.fasterxml.classmate.ResolvedTypeWithMembers;
+import com.fasterxml.classmate.members.HierarchicType;
+import com.fasterxml.classmate.members.ResolvedField;
+import com.fasterxml.classmate.members.ResolvedMethod;
+import com.github.victools.jsonschema.generator.MemberScope;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
+import com.github.victools.jsonschema.generator.TypeContext;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Generation context for collecting the members of a single type.
+ */
+class MemberCollectionContextImpl {
+
+    private static final Logger logger = LoggerFactory.getLogger(MemberCollectionContextImpl.class);
+
+    private final ResolvedType schemaTargetType;
+    private final SchemaGeneratorConfig generatorConfig;
+    private final TypeContext typeContext;
+    /*
+     * Collecting fields and methods according to their order in the Java Byte Code.
+     * Depending on the compiler implementation, that order may or may not match the declaration order in the source code.
+     */
+    private final Map<String, MemberScope<?, ?>> collectedProperties = new LinkedHashMap<>();
+    private final Set<String> requiredPropertyNames = new HashSet<>();
+
+    /**
+     * Constructor initialising the context with an empty set of collected properties.
+     *
+     * @param schemaTargetType targeted type for which to collect the contained fields and methods
+     * @param generatorConfig configuration object
+     * @param typeContext type context to consider when creating the respective {@code FieldScope} and {@code MethodScope} instances
+     */
+    public MemberCollectionContextImpl(ResolvedType schemaTargetType, SchemaGeneratorConfig generatorConfig, TypeContext typeContext) {
+        this.schemaTargetType = schemaTargetType;
+        this.generatorConfig = generatorConfig;
+        this.typeContext = typeContext;
+    }
+
+    /**
+     * Retrieve the collected properties in the order as per the configured property sorting mechanism.
+     *
+     * @return sorted collected properties
+     */
+    public List<MemberScope<?, ?>> getSortedProperties() {
+        return this.collectedProperties.values().stream()
+                .sorted(this.generatorConfig::sortProperties)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Retrieve the names (as to be included in the schema definition) of all required properties (in undefined order).
+     *
+     * @return names of required properties
+     */
+    public Set<String> getRequiredPropertyNames() {
+        return Collections.unmodifiableSet(this.requiredPropertyNames);
+    }
+
+    /**
+     * Recursively collect all properties of the given object type and add them to the respective maps.
+     */
+    public void collectProperties() {
+        logger.debug("collecting non-static fields and methods from {}", this.schemaTargetType);
+        ResolvedTypeWithMembers typeToCollectMembersFrom = this.typeContext.resolveWithMembers(this.schemaTargetType);
+        // member fields and methods are being collected from the targeted type as well as its super types
+        this.collectFields(typeToCollectMembersFrom.getMemberFields(), typeToCollectMembersFrom);
+        this.collectMethods(typeToCollectMembersFrom.getMemberMethods(), typeToCollectMembersFrom);
+
+        if (this.generatorConfig.shouldIncludeStaticFields() || this.generatorConfig.shouldIncludeStaticMethods()) {
+            // static fields and methods are being collected only for the targeted type itself, i.e. need to iterate over super types specifically
+            for (HierarchicType singleHierarchy : typeToCollectMembersFrom.allTypesAndOverrides()) {
+                this.collectStaticMembers(singleHierarchy);
+            }
+        }
+    }
+
+    private void collectStaticMembers(HierarchicType singleHierarchy) {
+        ResolvedType hierarchyType = singleHierarchy.getType();
+        logger.debug("collecting static fields and methods from {}", hierarchyType);
+        // static members need to be looked up from the declaring type directly as they are not inherited as such
+        ResolvedTypeWithMembers hierarchyTypeWithMembers = this.typeContext.resolveWithMembers(hierarchyType);
+        if (this.generatorConfig.shouldIncludeStaticFields()) {
+            this.collectFields(hierarchyTypeWithMembers.getStaticFields(), hierarchyTypeWithMembers);
+        }
+        if (this.generatorConfig.shouldIncludeStaticMethods()) {
+            this.collectMethods(hierarchyTypeWithMembers.getStaticMethods(), hierarchyTypeWithMembers);
+        }
+    }
+
+    /**
+     * Preparation Step: collect the designated fields.
+     *
+     * @param fields targeted fields from {@code typeToCollectMembersFrom}
+     * @param typeToCollectMembersFrom specific container of given fields (possibly a supertype or interface of the targeted type)
+     */
+    private void collectFields(ResolvedField[] fields, ResolvedTypeWithMembers typeToCollectMembersFrom) {
+        Stream.of(fields)
+                .map(declaredField -> this.typeContext.createFieldScope(declaredField, typeToCollectMembersFrom))
+                .filter(fieldScope -> !this.generatorConfig.shouldIgnore(fieldScope))
+                .forEach(this::collect);
+    }
+
+    /**
+     * Preparation Step: collect the designated methods.
+     *
+     * @param methods targeted methods from {@code typeToCollectMembersFrom}
+     * @param typeToCollectMembersFrom specific container of given fields (possibly a supertype or interface of the targeted type)
+     */
+    private void collectMethods(ResolvedMethod[] methods, ResolvedTypeWithMembers typeToCollectMembersFrom) {
+        Stream.of(methods)
+                .map(declaredMethod -> this.typeContext.createMethodScope(declaredMethod, typeToCollectMembersFrom))
+                .filter(methodScope -> !this.generatorConfig.shouldIgnore(methodScope))
+                .forEach(this::collect);
+    }
+
+    /**
+     * Add the given field or method to this context's list of collected properties.
+     *
+     * @param member field/method to add
+     */
+    public void collect(MemberScope<?, ?> member) {
+        if (member.isFakeContainerItemScope()) {
+            this.collectedProperties.put(member.getSchemaPropertyName(), member);
+            return;
+        }
+        MemberScope<?, ?> memberWithNameOverride = this.getMemberWithNameOverride(member);
+        this.registerIfRequired(memberWithNameOverride);
+        String propertyName = memberWithNameOverride.getSchemaPropertyName();
+        if (this.collectedProperties.containsKey(propertyName)) {
+            logger.debug("ignoring overridden {}.{}", memberWithNameOverride.getDeclaringType(), memberWithNameOverride.getDeclaredName());
+        } else {
+            this.collectedProperties.put(propertyName, memberWithNameOverride);
+        }
+    }
+
+    private MemberScope<?, ?> getMemberWithNameOverride(MemberScope<?, ?> member) {
+        String propertyNameOverride = member.getContext().performActionOnMember(member,
+                this.generatorConfig::resolvePropertyNameOverride, this.generatorConfig::resolvePropertyNameOverride);
+        if (propertyNameOverride == null) {
+            return member;
+        }
+        return member.withOverriddenName(propertyNameOverride);
+    }
+
+    private void registerIfRequired(MemberScope<?, ?> member) {
+        if (member.getContext().performActionOnMember(member, this.generatorConfig::isRequired, this.generatorConfig::isRequired)) {
+            this.requiredPropertyNames.add(member.getSchemaPropertyName());
+        }
+    }
+}
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 a5a4c82..4c4681c 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
@@ -17,10 +17,6 @@
 package com.github.victools.jsonschema.generator.impl;
 
 import com.fasterxml.classmate.ResolvedType;
-import com.fasterxml.classmate.ResolvedTypeWithMembers;
-import com.fasterxml.classmate.members.HierarchicType;
-import com.fasterxml.classmate.members.ResolvedField;
-import com.fasterxml.classmate.members.ResolvedMethod;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.BooleanNode;
@@ -439,15 +435,14 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
     }
 
     private JsonNode populateItemMemberSchema(TypeScope targetScope) {
-        JsonNode arrayItemDefinition;
         if (targetScope instanceof FieldScope && !((FieldScope) targetScope).isFakeContainerItemScope()) {
-            arrayItemDefinition = this.populateFieldSchema(((FieldScope) targetScope).asFakeContainerItemScope());
-        } else if (targetScope instanceof MethodScope && !((MethodScope) targetScope).isFakeContainerItemScope()) {
-            arrayItemDefinition = this.populateMethodSchema(((MethodScope) targetScope).asFakeContainerItemScope());
-        } else {
-            arrayItemDefinition = this.generatorConfig.createObjectNode();
-            this.traverseGenericType(targetScope.getContainerItemType(), (ObjectNode) arrayItemDefinition, false);
+            return this.populateFieldSchema(((FieldScope) targetScope).asFakeContainerItemScope());
+        }
+        if (targetScope instanceof MethodScope && !((MethodScope) targetScope).isFakeContainerItemScope()) {
+            return this.populateMethodSchema(((MethodScope) targetScope).asFakeContainerItemScope());
         }
+        ObjectNode arrayItemDefinition = this.generatorConfig.createObjectNode();
+        this.traverseGenericType(targetScope.getContainerItemType(), arrayItemDefinition, false);
         return arrayItemDefinition;
     }
 
@@ -460,156 +455,47 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
     private void generateObjectDefinition(ResolvedType targetType, ObjectNode definition) {
         definition.put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_OBJECT));
 
-        /*
-         * Collecting fields and methods according to their order in the Java Byte Code.
-         * Depending on the compiler implementation, that order may or may not match the declaration order in the source code.
-         */
-        final Map<String, MemberScope<?, ?>> targetProperties = new LinkedHashMap<>();
-        final Set<String> requiredProperties = new HashSet<>();
-
-        this.collectObjectProperties(targetType, targetProperties, requiredProperties);
+        MemberCollectionContextImpl memberCollectionContext = new MemberCollectionContextImpl(targetType, this.generatorConfig, this.typeContext);
+        memberCollectionContext.collectProperties();
 
-        if (targetProperties.isEmpty()) {
-            return;
+        List<MemberScope<?, ?>> sortedProperties = memberCollectionContext.getSortedProperties();
+        if (!sortedProperties.isEmpty()) {
+            this.addPropertiesToDefinition(definition, sortedProperties, memberCollectionContext.getRequiredPropertyNames());
         }
-        ObjectNode propertiesNode = this.generatorConfig.createObjectNode();
-        List<MemberScope<?, ?>> sortedProperties = targetProperties.values().stream()
-                .sorted(this.generatorConfig::sortProperties)
-                .collect(Collectors.toList());
+    }
+
+    private void addPropertiesToDefinition(ObjectNode definition, List<MemberScope<?, ?>> sortedProperties, Set<String> requiredPropertyNames) {
+        ObjectNode propertiesNode = definition.putObject(this.getKeyword(SchemaKeyword.TAG_PROPERTIES));
         Map<String, List<String>> dependentRequires = new LinkedHashMap<>();
         for (MemberScope<?, ?> property : sortedProperties) {
             this.addPropertiesEntry(propertiesNode, dependentRequires, property);
         }
-        definition.set(this.getKeyword(SchemaKeyword.TAG_PROPERTIES), propertiesNode);
-        if (!requiredProperties.isEmpty()) {
-            ArrayNode requiredNode = this.generatorConfig.createArrayNode();
+        if (!requiredPropertyNames.isEmpty()) {
+            ArrayNode requiredNode = definition.putArray(this.getKeyword(SchemaKeyword.TAG_REQUIRED));
             // list required properties in the same order as the property
             sortedProperties.stream()
                     .map(MemberScope::getSchemaPropertyName)
-                    .filter(requiredProperties::contains)
+                    .filter(requiredPropertyNames::contains)
                     .forEach(requiredNode::add);
-            definition.set(this.getKeyword(SchemaKeyword.TAG_REQUIRED), requiredNode);
         }
         if (!dependentRequires.isEmpty()) {
-            ObjectNode dependentRequiredNode = this.generatorConfig.createObjectNode();
+            ObjectNode dependentRequiredNode = definition.putObject(this.getKeyword(SchemaKeyword.TAG_DEPENDENT_REQUIRED));
             dependentRequires.forEach((leadName, dependentNames) -> dependentNames
                     .forEach(dependentRequiredNode.withArray(leadName)::add));
-            definition.set(this.getKeyword(SchemaKeyword.TAG_DEPENDENT_REQUIRED), dependentRequiredNode);
         }
     }
 
     private void addPropertiesEntry(ObjectNode propertiesNode, Map<String, List<String>> dependentRequires, MemberScope<?, ?> property) {
-        JsonNode subSchema;
-        List<String> dependentRequiredForProperty;
-        if (property instanceof FieldScope) {
-            subSchema = this.populateFieldSchema((FieldScope) property);
-            dependentRequiredForProperty = this.generatorConfig.resolveDependentRequires((FieldScope) property);
-        } else if (property instanceof MethodScope) {
-            subSchema = this.populateMethodSchema((MethodScope) property);
-            dependentRequiredForProperty = this.generatorConfig.resolveDependentRequires((MethodScope) property);
-        } else {
-            throw new IllegalStateException("Unsupported member scope of type " + property.getClass());
-        }
         String propertyName = property.getSchemaPropertyName();
-        propertiesNode.set(propertyName, subSchema);
-        if (dependentRequiredForProperty != null && !dependentRequiredForProperty.isEmpty()) {
-            dependentRequires.put(propertyName, dependentRequiredForProperty);
-        }
-    }
-
-    /**
-     * Recursively collect all properties of the given object type and add them to the respective maps.
-     *
-     * @param targetType the type for which to collect fields and methods
-     * @param targetProperties map of named fields and methods
-     * @param requiredProperties set of properties value required
-     */
-    private void collectObjectProperties(ResolvedType targetType, Map<String, MemberScope<?, ?>> targetProperties, Set<String> requiredProperties) {
-        logger.debug("collecting non-static fields and methods from {}", targetType);
-        final ResolvedTypeWithMembers targetTypeWithMembers = this.typeContext.resolveWithMembers(targetType);
-        // member fields and methods are being collected from the targeted type as well as its super types
-        this.collectFields(targetTypeWithMembers, ResolvedTypeWithMembers::getMemberFields, targetProperties, requiredProperties);
-        this.collectMethods(targetTypeWithMembers, ResolvedTypeWithMembers::getMemberMethods, targetProperties, requiredProperties);
-
-        if (this.generatorConfig.shouldIncludeStaticFields() || this.generatorConfig.shouldIncludeStaticMethods()) {
-            // static fields and methods are being collected only for the targeted type itself, i.e. need to iterate over super types specifically
-            for (HierarchicType singleHierarchy : targetTypeWithMembers.allTypesAndOverrides()) {
-                collectStaticMembers(singleHierarchy, targetProperties, requiredProperties);
-            }
-        }
-    }
 
-    private void collectStaticMembers(HierarchicType singleHierarchy,
-            Map<String, MemberScope<?, ?>> targetProperties, Set<String> requiredProperties) {
-        ResolvedType hierarchyType = singleHierarchy.getType();
-        logger.debug("collecting static fields and methods from {}", hierarchyType);
-        ResolvedTypeWithMembers hierarchyTypeMembers = this.typeContext.resolveWithMembers(hierarchyType);
-        if (this.generatorConfig.shouldIncludeStaticFields()) {
-            this.collectFields(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticFields, targetProperties, requiredProperties);
-        }
-        if (this.generatorConfig.shouldIncludeStaticMethods()) {
-            this.collectMethods(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticMethods, targetProperties, requiredProperties);
-        }
-    }
-
-    /**
-     * Preparation Step: add the designated fields to the specified {@link Map}.
-     *
-     * @param declaringTypeMembers the type declaring the fields to populate
-     * @param fieldLookup retrieval function for getter targeted fields from {@code declaringTypeMembers}
-     * @param collectedProperties collected fields and methods for which sub schemas should be added
-     * @param requiredProperties set of properties value required
-     */
-    private void collectFields(ResolvedTypeWithMembers declaringTypeMembers, Function<ResolvedTypeWithMembers, ResolvedField[]> fieldLookup,
-            Map<String, MemberScope<?, ?>> collectedProperties, Set<String> requiredProperties) {
-        Stream.of(fieldLookup.apply(declaringTypeMembers))
-                .map(declaredField -> this.typeContext.createFieldScope(declaredField, declaringTypeMembers))
-                .filter(fieldScope -> !this.generatorConfig.shouldIgnore(fieldScope))
-                .forEach(fieldScope -> this.collectField(fieldScope, collectedProperties, requiredProperties));
-    }
-
-    /**
-     * Preparation Step: add the designated methods to the specified {@link Map}.
-     *
-     * @param declaringTypeMembers the type declaring the methods to populate
-     * @param methodLookup retrieval function for getter targeted methods from {@code declaringTypeMembers}
-     * @param collectedProperties collected fields and methods for which sub schemas should be added
-     * @param requiredProperties set of properties value required
-     */
-    private void collectMethods(ResolvedTypeWithMembers declaringTypeMembers, Function<ResolvedTypeWithMembers, ResolvedMethod[]> methodLookup,
-            Map<String, MemberScope<?, ?>> collectedProperties, Set<String> requiredProperties) {
-        Stream.of(methodLookup.apply(declaringTypeMembers))
-                .map(declaredMethod -> this.typeContext.createMethodScope(declaredMethod, declaringTypeMembers))
-                .filter(methodScope -> !this.generatorConfig.shouldIgnore(methodScope))
-                .forEach(methodScope -> this.collectMethod(methodScope, collectedProperties, requiredProperties));
-    }
+        JsonNode subSchema = this.typeContext.performActionOnMember(property, this::populateFieldSchema, this::populateMethodSchema);
+        propertiesNode.set(propertyName, subSchema);
 
-    /**
-     * Preparation Step: add the given field to the specified {@link Map}.
-     *
-     * @param field declared field that should be added
-     * @param collectedProperties collection of fields/methods to be added as properties
-     * @param requiredProperties set of required properties
-     */
-    private void collectField(FieldScope field, Map<String, MemberScope<?, ?>> collectedProperties, Set<String> requiredProperties) {
-        final FieldScope fieldWithNameOverride;
-        final String propertyName;
-        if (field.isFakeContainerItemScope()) {
-            fieldWithNameOverride = field;
-            propertyName = field.getSchemaPropertyName();
-        } else {
-            String propertyNameOverride = this.generatorConfig.resolvePropertyNameOverride(field);
-            fieldWithNameOverride = propertyNameOverride == null ? field : field.withOverriddenName(propertyNameOverride);
-            propertyName = fieldWithNameOverride.getSchemaPropertyName();
-            if (this.generatorConfig.isRequired(field)) {
-                requiredProperties.add(propertyName);
-            }
-            if (collectedProperties.containsKey(propertyName)) {
-                logger.debug("ignoring overridden {}.{}", fieldWithNameOverride.getDeclaringType(), fieldWithNameOverride.getDeclaredName());
-                return;
-            }
+        List<String> dependentRequiredForProperty = this.typeContext.performActionOnMember(property,
+                this.generatorConfig::resolveDependentRequires, this.generatorConfig::resolveDependentRequires);
+        if (!Util.isNullOrEmpty(dependentRequiredForProperty)) {
+            dependentRequires.put(propertyName, dependentRequiredForProperty);
         }
-        collectedProperties.put(propertyName, fieldWithNameOverride);
     }
 
     /**
@@ -618,7 +504,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      * @param field field/property to populate the schema node for
      * @return schema node representing the given field/property
      */
-    private ObjectNode populateFieldSchema(FieldScope field) {
+    private JsonNode populateFieldSchema(FieldScope field) {
         List<ResolvedType> typeOverrides = this.generatorConfig.resolveTargetTypeOverrides(field);
         if (typeOverrides == null && this.generatorConfig.shouldTransparentlyResolveSubtypesOfMembers()) {
             typeOverrides = this.generatorConfig.resolveSubtypes(field.getType(), this);
@@ -638,15 +524,20 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         if (fieldOptions.size() == 1) {
             return this.createFieldSchema(new MemberDetails<>(fieldOptions.get(0), isNullable, false, null));
         }
+        return this.createMemberSchemaWithMultipleOptions(fieldOptions, isNullable, this::createFieldSchema);
+    }
+
+    private <M extends MemberScope<?, ?>> JsonNode createMemberSchemaWithMultipleOptions(List<M> memberOptions, boolean isNullable,
+            Function<MemberDetails<M>, JsonNode> createMemberSchema) {
         ObjectNode subSchema = this.generatorConfig.createObjectNode();
         ArrayNode anyOfArray = subSchema.withArray(this.getKeyword(SchemaKeyword.TAG_ANYOF));
         if (isNullable) {
             anyOfArray.addObject()
                     .put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_NULL));
         }
-        fieldOptions.stream()
+        memberOptions.stream()
                 .map(option -> new MemberDetails<>(option, false, false, null))
-                .map(this::createFieldSchema)
+                .map(createMemberSchema)
                 .forEach(anyOfArray::add);
         return subSchema;
     }
@@ -664,34 +555,6 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         return subSchema;
     }
 
-    /**
-     * Preparation Step: add the given method to the specified {@link Map}.
-     *
-     * @param method declared method that should be added to the specified node
-     * @param collectedProperties collection of fields/methods to be added as properties
-     * @param requiredProperties set of properties value required
-     */
-    private void collectMethod(MethodScope method, Map<String, MemberScope<?, ?>> collectedProperties, Set<String> requiredProperties) {
-        final MethodScope methodWithNameOverride;
-        final String propertyName;
-        if (method.isFakeContainerItemScope()) {
-            methodWithNameOverride = method;
-            propertyName = method.getSchemaPropertyName();
-        } else {
-            String propertyNameOverride = this.generatorConfig.resolvePropertyNameOverride(method);
-            methodWithNameOverride = propertyNameOverride == null ? method : method.withOverriddenName(propertyNameOverride);
-            propertyName = methodWithNameOverride.getSchemaPropertyName();
-            if (this.generatorConfig.isRequired(method)) {
-                requiredProperties.add(propertyName);
-            }
-            if (collectedProperties.containsKey(propertyName)) {
-                logger.debug("ignoring overridden {}.{}", methodWithNameOverride.getDeclaringType(), methodWithNameOverride.getDeclaredName());
-                return;
-            }
-        }
-        collectedProperties.put(propertyName, methodWithNameOverride);
-    }
-
     /**
      * Preparation Step: create a node for a schema representing the given method's associated return type.
      *
@@ -718,18 +581,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         if (methodOptions.size() == 1) {
             return this.createMethodSchema(new MemberDetails<>(methodOptions.get(0), isNullable, false, null));
         }
-        ObjectNode subSchema = this.generatorConfig.createObjectNode();
-        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
-                .stream()
-                .map(option -> new MemberDetails<>(option, false, false, null))
-                .map(this::createMethodSchema)
-                .forEach(anyOfArray::add);
-        return subSchema;
+        return this.createMemberSchemaWithMultipleOptions(methodOptions, isNullable, this::createMethodSchema);
     }
 
     /**
@@ -756,7 +608,6 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      * @param memberDetails details for field/method to populate schema for
      * @param collectedMemberAttributes separately collected attribute for the field/method in their respective declaring type
      * @see #populateFieldSchema(FieldScope)
-     * @see #collectMethod(MethodScope, Map, Set)
      */
     private <M extends MemberScope<?, ?>> void populateMemberSchema(ObjectNode targetNode, MemberDetails<M> memberDetails,
             ObjectNode collectedMemberAttributes) {
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/AdditionalPropertiesModule.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/AdditionalPropertiesModule.java
index 0467373..8d04c6d 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/AdditionalPropertiesModule.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/AdditionalPropertiesModule.java
@@ -64,14 +64,9 @@ public class AdditionalPropertiesModule implements Module {
         if (valueType == null || valueType.getErasedType() == Object.class) {
             return null;
         }
-        MemberScope<?, ?> mapValueScope = member.asFakeContainerItemScope(Map.class, 1);
-        if (mapValueScope instanceof FieldScope) {
-            return context.createStandardDefinitionReference((FieldScope) mapValueScope, null);
-        }
-        if (mapValueScope instanceof MethodScope) {
-            return context.createStandardDefinitionReference((MethodScope) mapValueScope, null);
-        }
-        throw new IllegalStateException("Unsupported member scope type: " + member.getClass());
+        return member.getContext().performActionOnMember(member.asFakeContainerItemScope(Map.class, 1),
+                field -> context.createStandardDefinitionReference(field, null),
+                method -> context.createStandardDefinitionReference(method, null));
     }
 
     /**
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/ConstantValueModule.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/ConstantValueModule.java
index 94b25de..e23bffa 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/ConstantValueModule.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/ConstantValueModule.java
@@ -35,21 +35,25 @@ public class ConstantValueModule implements Module {
      * @return collection containing single constant value; returning null if field has no constant value
      */
     private static List<?> extractConstantFieldValue(FieldScope field) {
-        if (field.isStatic() && field.isFinal() && !field.getRawMember().isEnumConstant() && !field.isFakeContainerItemScope()) {
-            Field rawField = field.getRawMember();
-            try {
-                return Collections.singletonList(rawField.get(null));
-            } catch (IllegalAccessException ex) {
-                // let's try to make it accessible then
-            }
-            try {
-                rawField.setAccessible(true);
-                return Collections.singletonList(rawField.get(null));
-            } catch (SecurityException | IllegalAccessException ex) {
-                // if the field cannot be accessed, we just can't extract any constant value
-            }
+        if (!field.isStatic() || !field.isFinal()) {
+            return null;
+        }
+        if (field.isFakeContainerItemScope() || field.getRawMember().isEnumConstant()) {
+            return null;
+        }
+        Field rawField = field.getRawMember();
+        try {
+            return Collections.singletonList(rawField.get(null));
+        } catch (IllegalAccessException ex) {
+            // let's try to make it accessible then
+        }
+        try {
+            rawField.setAccessible(true);
+            return Collections.singletonList(rawField.get(null));
+        } catch (SecurityException | IllegalAccessException ex) {
+            // if the field cannot be accessed, we just can't extract any constant value
+            return null;
         }
-        return null;
     }
 
     /**


Test patch:
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/AbstractTypeAwareTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/AbstractTypeAwareTest.java
index b554bd0..71d9f18 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/AbstractTypeAwareTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/AbstractTypeAwareTest.java
@@ -45,7 +45,7 @@ public class AbstractTypeAwareTest {
      * @param schemaVersion designated JSON Schema version
      */
     protected void prepareContextForVersion(SchemaVersion schemaVersion) {
-        TypeContext typeContext = TypeContextFactory.createDefaultTypeContext();
+        TypeContext typeContext = Mockito.spy(TypeContextFactory.createDefaultTypeContext());
         ResolvedType resolvedTestClass = typeContext.resolve(this.testClass);
         this.testClassMembers = typeContext.resolveWithMembers(resolvedTestClass);
         this.context = Mockito.mock(SchemaGenerationContext.class, Mockito.RETURNS_DEEP_STUBS);
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/MethodScopeTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/MethodScopeTest.java
index fa91e40..78df75a 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/MethodScopeTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/MethodScopeTest.java
@@ -27,6 +27,7 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mockito;
 
 /**
  * Test for the {@link MethodScope} class.
@@ -116,6 +117,25 @@ public class MethodScopeTest extends AbstractTypeAwareTest {
         }
     }
 
+    @ParameterizedTest
+    @CsvSource({
+        "getCalculatedValue, false, getCalculatedValue()",
+        "isBehavingSomehow, false, isBehavingSomehow()",
+        "getxIndex, false, getxIndex()",
+        "isURL, false, isURL()",
+        "getCalculatedValue, true, calculatedValue",
+        "isBehavingSomehow, true, behavingSomehow",
+        "getxIndex, true, xIndex",
+        "isURL, true, URL",
+    })
+    public void testGetSchemaPropertyName(String methodName, boolean deriveFieldNameFromArgumentFreeMethod, String expectedPropertyName) {
+        MethodScope method = this.getTestClassMethod(methodName);
+        Mockito.when(this.getContext().getTypeContext().isDerivingFieldsFromArgumentFreeMethods())
+                .thenReturn(deriveFieldNameFromArgumentFreeMethod);
+
+        Assertions.assertEquals(expectedPropertyName, method.getSchemaPropertyName());
+    }
+
     private static class TestClass {
 
         private int fieldWithPrivateGetter;
@@ -177,6 +197,14 @@ public class MethodScopeTest extends AbstractTypeAwareTest {
             return false;
         }
 
+        public int getxIndex() {
+            return 1;
+        }
+
+        public boolean isURL() {
+            return false;
+        }
+
         public int calculateSomething() {
             return 42;
         }