diff --git a/CHANGELOG.md b/CHANGELOG.md index c101815b..980d06df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### `jsonschema-generator` +#### Changed +- consider JavaBeans API specification in getter naming convention for field names with the second character being uppercase (e.g., a field `xIndex` has the getter `getxIndex()` according to the specification) +- allow for field names starting with `is` to have getter of the same name (e.g., a field `isBool` may have the getter `isBool()`) + ### `jsonschema-module-jackson` #### Added - elevate nested properties to the parent type where members are annotated with `@JsonUnwrapped` 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 121813fa..4ac79dca 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 @@ -24,6 +24,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; @@ -112,14 +114,25 @@ public MethodScope findGetter() { * @return public getter from within the field's declaring class */ private MethodScope doFindGetter() { - String capitalisedFieldName = this.getDeclaredName().substring(0, 1).toUpperCase() + this.getDeclaredName().substring(1); - String getterName1 = "get" + capitalisedFieldName; - String getterName2 = "is" + capitalisedFieldName; + String declaredName = this.getDeclaredName(); + Set possibleGetterNames = new HashSet<>(5); + // @since 4.32.0 - for a field like "xIndex" also consider "getxIndex()" as getter method (according to JavaBeans specification) + if (declaredName.length() > 1 && Character.isUpperCase(declaredName.charAt(1))) { + possibleGetterNames.add("get" + declaredName); + possibleGetterNames.add("is" + declaredName); + } + // common naming convention: capitalise first character and leave the rest as-is + String capitalisedFieldName = declaredName.substring(0, 1).toUpperCase() + declaredName.substring(1); + 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))) { + possibleGetterNames.add(declaredName); + } ResolvedMethod[] methods = this.getDeclaringTypeMembers().getMemberMethods(); return Stream.of(methods) - .filter(method -> method.getRawMember().getParameterCount() == 0) - .filter(ResolvedMethod::isPublic) - .filter(method -> method.getName().equals(getterName1) || method.getName().equals(getterName2)) + .filter(method -> method.isPublic() && method.getRawMember().getParameterCount() == 0) + .filter(method -> possibleGetterNames.contains(method.getName())) .findFirst() .map(method -> this.getContext().createMethodScope(method, this.getDeclaringTypeMembers())) .orElse(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 bdc9b035..0d92bdf1 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,7 +24,9 @@ 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.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -129,24 +131,36 @@ private FieldScope doFindGetterField() { return null; } String methodName = this.getDeclaredName(); - String fieldName; - if (methodName.startsWith("get") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) { - // ensure that the variable starts with a lower-case letter - fieldName = methodName.substring(3, 4).toLowerCase() + methodName.substring(4); - } else if (methodName.startsWith("is") && methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) { - // ensure that the variable starts with a lower-case letter - fieldName = methodName.substring(2, 3).toLowerCase() + methodName.substring(3); - } else { - // method name does not fall into getter conventions - fieldName = null; + Set possibleFieldNames = new HashSet<>(3); + if (methodName.startsWith("get")) { + 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)); + } + } else if (methodName.startsWith("is")) { + if (methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) { + // 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); + } + // @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 (fieldName == null) { + if (possibleFieldNames.isEmpty()) { + // method name does not fall into getter conventions return null; } // method name matched getter conventions // check whether a matching field exists return Stream.of(this.getDeclaringTypeMembers().getMemberFields()) - .filter(memberField -> memberField.getName().equals(fieldName)) + .filter(memberField -> possibleFieldNames.contains(memberField.getName())) .findFirst() .map(field -> this.getContext().createFieldScope(field, this.getDeclaringTypeMembers())) .orElse(null); diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/FieldScopeTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/FieldScopeTest.java index d3e1be9a..e417403a 100644 --- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/FieldScopeTest.java +++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/FieldScopeTest.java @@ -49,7 +49,13 @@ static Stream parametersForTestFindGetter() { Arguments.of("fieldWithPrivateGetter", null, null), Arguments.of("fieldWithPublicGetter", null, "getFieldWithPublicGetter"), Arguments.of("fieldWithPublicGetter", "fieldWithoutGetter", "getFieldWithPublicGetter"), - Arguments.of("fieldWithPublicBooleanGetter", null, "isFieldWithPublicBooleanGetter") + Arguments.of("fieldWithPublicBooleanGetter", null, "isFieldWithPublicBooleanGetter"), + Arguments.of("isFieldWithMatchingGetter", null, "isFieldWithMatchingGetter"), + Arguments.of("isFieldWithMatchingGetter", "fieldWithPublicGetter", "isFieldWithMatchingGetter"), + Arguments.of("aFieldWithJavaBeansConformingGetter", null, "getaFieldWithJavaBeansConformingGetter"), + Arguments.of("aFieldWithJavaBeansNonConformingGetter", null, "getAFieldWithJavaBeansNonConformingGetter"), + Arguments.of("aFieldWithJavaBeansConformingBooleanGetter", null, "isaFieldWithJavaBeansConformingBooleanGetter"), + Arguments.of("aFieldWithJavaBeansNonConformingBooleanGetter", null, "isAFieldWithJavaBeansNonConformingBooleanGetter") ); } @@ -107,6 +113,11 @@ private static class TestClass { private int fieldWithPrivateGetter; private long fieldWithPublicGetter; private boolean fieldWithPublicBooleanGetter; + private boolean isFieldWithMatchingGetter; + private int aFieldWithJavaBeansConformingGetter; + private long aFieldWithJavaBeansNonConformingGetter; + private boolean aFieldWithJavaBeansConformingBooleanGetter; + private boolean aFieldWithJavaBeansNonConformingBooleanGetter; private int getFieldWithPrivateGetter() { return this.fieldWithPrivateGetter; @@ -120,6 +131,26 @@ public long getFieldWithPublicGetter() { public boolean isFieldWithPublicBooleanGetter() { return this.fieldWithPublicBooleanGetter; } + + public boolean isFieldWithMatchingGetter() { + return this.isFieldWithMatchingGetter; + } + + public int getaFieldWithJavaBeansConformingGetter() { + return this.aFieldWithJavaBeansConformingGetter; + } + + public long getAFieldWithJavaBeansNonConformingGetter() { + return this.aFieldWithJavaBeansNonConformingGetter; + } + + public boolean isaFieldWithJavaBeansConformingBooleanGetter() { + return this.aFieldWithJavaBeansConformingBooleanGetter; + } + + public boolean isAFieldWithJavaBeansNonConformingBooleanGetter() { + return this.aFieldWithJavaBeansNonConformingBooleanGetter; + } } @Retention(RetentionPolicy.RUNTIME) 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 51de57d7..fa91e406 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 @@ -50,6 +50,12 @@ static Stream parametersForTestFindGetterField() { Arguments.of("getFieldWithPublicGetter", "getFieldWithPrivateGetter", "fieldWithPublicGetter"), Arguments.of("isFieldWithPublicBooleanGetter", null, "fieldWithPublicBooleanGetter"), Arguments.of("isFieldWithPublicBooleanGetter", "isBehavingSomehow", "fieldWithPublicBooleanGetter"), + Arguments.of("isFieldWithMatchingPublicGetter", null, "isFieldWithMatchingPublicGetter"), + Arguments.of("isFieldWithMatchingPublicGetter", "isFieldWithPublicBooleanGetter", "isFieldWithMatchingPublicGetter"), + Arguments.of("getaFieldWithJavaBeansConformingGetter", null, "aFieldWithJavaBeansConformingGetter"), + Arguments.of("getAFieldWithJavaBeansNonConformingGetter", null, "aFieldWithJavaBeansNonConformingGetter"), + Arguments.of("isaFieldWithJavaBeansConformingBooleanGetter", null, "aFieldWithJavaBeansConformingBooleanGetter"), + Arguments.of("isAFieldWithJavaBeansNonConformingBooleanGetter", null, "aFieldWithJavaBeansNonConformingBooleanGetter"), Arguments.of("getCalculatedValue", null, null), Arguments.of("isBehavingSomehow", null, null), Arguments.of("isBehavingSomehow", "isFieldWithPublicBooleanGetter", null), @@ -116,6 +122,11 @@ private static class TestClass { private long fieldWithPublicGetter; @TestAnnotation private boolean fieldWithPublicBooleanGetter; + private boolean isFieldWithMatchingPublicGetter; + private int aFieldWithJavaBeansConformingGetter; + private long aFieldWithJavaBeansNonConformingGetter; + private boolean aFieldWithJavaBeansConformingBooleanGetter; + private boolean aFieldWithJavaBeansNonConformingBooleanGetter; @TestAnnotation private int getFieldWithPrivateGetter() { @@ -130,6 +141,26 @@ public boolean isFieldWithPublicBooleanGetter() { return this.fieldWithPublicBooleanGetter; } + public boolean isFieldWithMatchingPublicGetter() { + return this.isFieldWithMatchingPublicGetter; + } + + public int getaFieldWithJavaBeansConformingGetter() { + return this.aFieldWithJavaBeansConformingGetter; + } + + public long getAFieldWithJavaBeansNonConformingGetter() { + return this.aFieldWithJavaBeansNonConformingGetter; + } + + public boolean isaFieldWithJavaBeansConformingBooleanGetter() { + return this.aFieldWithJavaBeansConformingBooleanGetter; + } + + public boolean isAFieldWithJavaBeansNonConformingBooleanGetter() { + return this.aFieldWithJavaBeansNonConformingBooleanGetter; + } + public double getCalculatedValue() { return 42.; }