-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
KotlinJacksonModule supports JsonProperty annotations on Kotlin data classes #359
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
|
||
<properties> | ||
<java.module.name>com.github.victools.jsonschema.module.jackson</java.module.name> | ||
<kotlin.version>1.9.0-Beta</kotlin.version> | ||
</properties> | ||
|
||
<dependencies> | ||
|
@@ -34,6 +35,17 @@ | |
<artifactId>jackson-annotations</artifactId> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jetbrains.kotlin</groupId> | ||
<artifactId>kotlin-stdlib</artifactId> | ||
<version>${kotlin.version}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jetbrains.kotlin</groupId> | ||
<artifactId>kotlin-test</artifactId> | ||
<version>${kotlin.version}</version> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
|
@@ -54,7 +66,20 @@ | |
<groupId>org.moditect</groupId> | ||
<artifactId>moditect-maven-plugin</artifactId> | ||
</plugin> | ||
<plugin> | ||
<groupId>org.jetbrains.kotlin</groupId> | ||
<artifactId>kotlin-maven-plugin</artifactId> | ||
<version>${kotlin.version}</version> | ||
<extensions>true</extensions> | ||
<configuration> | ||
<jvmTarget>1.8</jvmTarget> | ||
</configuration> | ||
</plugin> | ||
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-compiler-plugin</artifactId> | ||
</plugin> | ||
Comment on lines
+75
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The Maven compiler plugin is the first one to be mentioned in this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to Kotlin docs (https://kotlinlang.org/docs/maven.html#compile-kotlin-and-java-sources):
That's why the additional |
||
</plugins> | ||
</build> | ||
|
||
</project> | ||
</project> |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,62 @@ | ||||||
package com.github.victools.jsonschema.module.jackson; | ||||||
|
||||||
import com.fasterxml.classmate.members.RawField; | ||||||
import com.fasterxml.jackson.annotation.JsonProperty; | ||||||
import com.github.victools.jsonschema.generator.MemberScope; | ||||||
import java.lang.reflect.Parameter; | ||||||
import java.util.List; | ||||||
import java.util.OptionalInt; | ||||||
import kotlin.Metadata; | ||||||
|
||||||
public class KotlinJacksonModule extends JacksonModule { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Let's please not introduce an alternative module just for Kotlin. Instead, we should extend the |
||||||
/** | ||||||
* Look up an alternative name for a member in the constructor parameter list. | ||||||
* When the Kotlin compiler compiles a Kotlin data class, it creates a constructor with a parameter | ||||||
* for each field in the data class. The parameters have generic name such as "arg0", "arg1", etc. | ||||||
* In order to find the appropriate constructor parameter, the method first determines the index of | ||||||
* specified member within its type member list and uses the parameter with the same index. | ||||||
* In order to avoid erroneous overrides, this method verifies that the specified member is indeed of a | ||||||
* Kotlin class. | ||||||
Comment on lines
+18
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: I understand your motivation but would prefer to not make this Kotlin specific. Perhaps other JVM languages have similar behavior. It'd be a pity to exclude those without any real need. |
||||||
* | ||||||
* @param member field/method to look-up alternative property name for | ||||||
* @return alternative property name or the base class implementation return value | ||||||
*/ | ||||||
@Override | ||||||
protected String getPropertyNameOverrideBasedOnJsonPropertyAnnotation(MemberScope<?, ?> member) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: There are other aspects where Jackson annotations are being considered -- not just for the property name overrides. It'd probably be worth it to generalize this a bit more to extend the coverage. |
||||||
if (isKotlinType(member)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: As per another comment, let's not limit this to Kotlin types alone. |
||||||
OptionalInt memberIndex = getMemberIndex(member); | ||||||
if (memberIndex.isPresent()) { | ||||||
Parameter[] parameters = getConstructorParameters(member); | ||||||
Parameter parameter = parameters[memberIndex.getAsInt()]; | ||||||
JsonProperty jsonPropertyAnnotation = parameter.getAnnotation(JsonProperty.class); | ||||||
if (jsonPropertyAnnotation != null) { | ||||||
String nameOverride = jsonPropertyAnnotation.value(); | ||||||
if (isValidNameOverride(member, nameOverride)) { | ||||||
return nameOverride; | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
return super.getPropertyNameOverrideBasedOnJsonPropertyAnnotation(member); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: I'd still favor annotations on the field and its getter method before even considering to check the constructor parameters. |
||||||
|
||||||
} | ||||||
|
||||||
private OptionalInt getMemberIndex(MemberScope<?, ?> member) { | ||||||
List<RawField> memberFields = member.getDeclaringType().getMemberFields(); | ||||||
for (int i = 0; i < memberFields.size(); i++) { | ||||||
if (memberFields.get(i).getName().equals(member.getName())) { | ||||||
return OptionalInt.of(i); | ||||||
} | ||||||
} | ||||||
return OptionalInt.empty(); | ||||||
} | ||||||
|
||||||
private static Parameter[] getConstructorParameters(MemberScope<?, ?> member) { | ||||||
return member.getDeclaringType().getConstructors().get(0).getRawMember().getParameters(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (non-blocking): Shouldn't we assume that multiple constructors could also exist? Only considering the first might be sufficient for Kotlin data classes, but perhaps more general support of annotations on constructor parameters would be achieved by taking others into account as well?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For Kotlin data classes, we can safely assume that all fields are initialized by the implicit ctor in their order in the field list. I'm afraid that might not be the case for other non-data JVM classes. e.g. class CtorTest{
final int bar;
@JsonProperty("my_bar")
final String foo;
public CtorTest(@JsonProperty("my_foo") int foo, String bar){
this.bar=foo;
this.foo=bar;
}
} How would you suggest we associate between fields and ctor parameters in the general case (non-Kotlin data classes)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @moranlefler I don't think we need to support such a mix of member and constructor parameter annotations. First of all: it's probably a good idea to put this new feature behind a dedicated I would have preferred matching by parameter name, but if I understood your code comment correctly, for the Kotlin data class that wouldn't work as they are generically called |
||||||
} | ||||||
|
||||||
private static boolean isKotlinType(MemberScope<?, ?> member) { | ||||||
return member.getDeclaringType().getErasedType().isAnnotationPresent(Metadata.class); | ||||||
} | ||||||
Comment on lines
+58
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: This check alone doesn't seem worth it to add an extra dependency for all consumers of this library. |
||||||
|
||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.github.victools.jsonschema.module.jackson | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty | ||
import com.github.victools.jsonschema.generator.* | ||
import org.junit.jupiter.api.Assertions | ||
import org.junit.jupiter.api.Test | ||
import java.util.* | ||
|
||
class KotlinJacksonModuleTest { | ||
data class TestJsonProperty( | ||
@JsonProperty("my_text") val text: String, | ||
@JsonProperty("my_number") val number: Int) | ||
|
||
@Test | ||
fun `naming override in kotlin data class with JsonProperty`() { | ||
val config = SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) | ||
.with(KotlinJacksonModule()) | ||
.build() | ||
|
||
val generator = SchemaGenerator(config) | ||
val result = generator.generateSchema(TestJsonProperty::class.java) | ||
val propertiesNode = result[config.getKeyword(SchemaKeyword.TAG_PROPERTIES)] | ||
val propertyNames: MutableSet<String> = TreeSet() | ||
propertiesNode.fieldNames().forEachRemaining { e: String -> propertyNames.add(e) } | ||
Assertions.assertTrue(propertyNames.contains("my_text")) | ||
Assertions.assertTrue(propertyNames.contains("my_number")) | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I'd prefer to avoid adding this extra dependency for all (also non-Kotlin) users.
It is only used to detect Kotlin classes, which can just as easily be steered through a
JacksonOption
and then be applied to all types equally.At the same time I was just wondering: since the Jackson library supports those annotations on constructor parameters as well (also in Java classes), there is nothing Kotlin-specific about this functionality then.
It just happens to apply to Kotlin data classes too, sure, but the more generic feature to be added here would be to always consider constructor parameters as well as annotations on the field and its getter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, would you prefer checking for the annotation in the appropriate ctor parameter in
FieldScope.getAnnotationConsideringFieldAndGetter
? We can add anOption
to enable this extra check