Skip to content
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

Fix snake case deserialization #189

Draft
wants to merge 3 commits into
base: 2.19
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions jr-annotation-support/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@
<groupId>de.jjohannes</groupId>
<artifactId>gradle-module-metadata-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>16</source>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is just for testing -- Jackson 2.x is JDK 8, still.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking a look at this draft PR. I appreciate the time and effort you invest in this open-source project :) The code in this PR still needs some refinement. I already expected that changing the JDK version is not wishful. This is just the first draft, where I focused on getting the functionality to work and gaining a better understanding of the library's internals.

Can I ask you a question about the RecordsHelper class? I've changed the visibility from default to public to use it in the AnnotationBasedIntrosepector class. I'm not sure if this change is appropriate. What do you think?

<target>16</target>
</configuration>
</plugin>
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.fasterxml.jackson.jr.ob.impl.JSONReader;
import com.fasterxml.jackson.jr.ob.impl.JSONWriter;
import com.fasterxml.jackson.jr.ob.impl.POJODefinition;
import com.fasterxml.jackson.jr.ob.impl.RecordsHelpers;

/**
*
Expand Down Expand Up @@ -91,18 +92,30 @@ protected POJODefinition introspectDefinition()
constructors = null;
} else {
constructors = new BeanConstructors(_type);
for (Constructor<?> ctor : _type.getDeclaredConstructors()) {
Class<?>[] argTypes = ctor.getParameterTypes();
if (argTypes.length == 0) {
constructors.addNoArgsConstructor(ctor);
} else if (argTypes.length == 1) {
Class<?> argType = argTypes[0];
if (argType == String.class) {
constructors.addStringConstructor(ctor);
} else if (argType == Integer.class || argType == Integer.TYPE) {
constructors.addIntConstructor(ctor);
} else if (argType == Long.class || argType == Long.TYPE) {
constructors.addLongConstructor(ctor);
if (RecordsHelpers.isRecordType(_type)) {
Constructor<?> canonical = RecordsHelpers.findCanonicalConstructor(_type);
constructors.addRecordConstructor(canonical);
for (int i = 0; i < canonical.getParameterCount(); i++) {
Parameter ctorParam = canonical.getParameters()[i];
final String explName = _findExplicitName(ctorParam);
if (explName != null) {
constructors.addRecordConstructorAlias(explName, ctorParam.getType(), i);
}
}
} else {
for (Constructor<?> ctor : _type.getDeclaredConstructors()) {
Class<?>[] argTypes = ctor.getParameterTypes();
if (argTypes.length == 0) {
constructors.addNoArgsConstructor(ctor);
} else if (argTypes.length == 1) {
Class<?> argType = argTypes[0];
if (argType == String.class) {
constructors.addStringConstructor(ctor);
} else if (argType == Integer.class || argType == Integer.TYPE) {
constructors.addIntConstructor(ctor);
} else if (argType == Long.class || argType == Long.TYPE) {
constructors.addLongConstructor(ctor);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.jr.ob.JSON;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -34,6 +35,11 @@ public String getMiddle() {
public void setLast(String str) { last = str; }
}

record SnakeCaseRecord(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot be done here, as it requires JDK beyond 8. Any Record tests need to go in separate jr-record-test Maven sub-project.

@JsonProperty("first_name") String firstName,
@JsonProperty("last_name") String lastName
) {}

/*
/**********************************************************************
/* Test methods
Expand Down Expand Up @@ -62,4 +68,20 @@ public void testSimpleAliases() throws Exception
assertEquals("Bob", result.middle);
assertEquals("Burger", result.last);
}

public void testSnakeCaseRecordDeserialization() throws Exception
{
final String input = a2q("{ 'first_name':'John', 'last_name':'Doe' }");
SnakeCaseRecord result;

// First: without setting, nothing matches
result = JSON.std.beanFrom(SnakeCaseRecord.class, input);
assertNull(result.firstName());
assertNull(result.lastName());

// but with annotations it's all good...
result = JSON_WITH_ANNO.beanFrom(SnakeCaseRecord.class, input);
assertEquals("John", result.firstName());
assertEquals("Doe", result.lastName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.jr.ob.JSON;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -47,6 +48,11 @@ public XYZ(int x, int y, int z) {
}
}

record SnakeCaseRecord(
@JsonIgnore int x,
@JsonProperty("y_value") int y
) {}

private final JSON JSON_WITH_ANNO = jsonWithAnnotationSupport();
private final JSON JSON_WITH_ANNO_WITH_STATIC =
JSON.builder().register(JacksonAnnotationExtension.std).enable(JSON.Feature.INCLUDE_STATIC_FIELDS).build();
Expand Down Expand Up @@ -118,6 +124,23 @@ public void testPropertyIgnoreWithUnknown() throws Exception
assertEquals(new XY().x, result.x);
}

// Probably another bug that needs to be fixed. First I'm focusing on the deserialization of records.
//
// public void testSnakeCaseRecordDeserialization() throws Exception
// {
// final String input = a2q("{ 'x':1, 'y_value':2 }");
// SnakeCaseRecord result;
//
// // First: without setting, nothing matches
// result = JSON.std.beanFrom(SnakeCaseRecord.class, input);
// assertNull(result);
//
// // but with annotations it's all good...
// result = JSON_WITH_ANNO.beanFrom(SnakeCaseRecord.class, input);
// assertEquals(0, result.x());
// assertEquals(2, result.y());
// }

/*
/**********************************************************************
/* Tests for @JsonIgnoreProperties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ public static SubclassingEnum from(String id) {
}
}


record SnakeCaseRecord(
@JsonProperty("first_name") String firstName,
@JsonProperty("last_name") String lastName
) {}

/*
/**********************************************************************
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.fasterxml.jackson.jr.annotationsupport;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.jr.ob.JSON;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -40,6 +40,11 @@ public FullNameBean(String f, String m, String l) {
public void setLast(String n) { last = n; }
}

record SnakeCaseRecord(
@JsonProperty("first_name") String firstName,
@JsonProperty("last_name") String lastName
) {}

private final JSON JSON_WITH_ANNO = jsonWithAnnotationSupport();

@Test
Expand Down Expand Up @@ -73,4 +78,20 @@ public void testPartialReorder() throws Exception
// and ensure no leakage to default one:
assertEquals(EXP_DEFAULT, JSON.std.asString(input));
}

public void testSnakeCaseRecordDeserialization() throws Exception
{
final String input = a2q("{ 'first_name':'John', 'last_name':'Doe' }");
SnakeCaseRecord result;

// First: without setting, nothing matches
result = JSON.std.beanFrom(SnakeCaseRecord.class, input);
assertNull(result.firstName());
assertNull(result.lastName());

// but with annotations it's all good...
result = JSON_WITH_ANNO.beanFrom(SnakeCaseRecord.class, input);
assertEquals("John", result.firstName());
assertEquals("Doe", result.lastName());
}
}
8 changes: 8 additions & 0 deletions jr-objects/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ has no other dependencies, and provides additional builder-style content generat
<groupId>de.jjohannes</groupId>
<artifactId>gradle-module-metadata-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>16</source>
<target>16</target>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not acceptable. Only "jr-record-test" can rely on later JDK than 8.

</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.fasterxml.jackson.jr.ob.impl;

import com.fasterxml.jackson.jr.ob.api.ValueReader;

import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

/**
* Container class added to encapsulate details of collection and use of
Expand All @@ -10,6 +14,9 @@
*/
public class BeanConstructors
{
public record RecordAlias(ValueReader reader, Integer pos) {

}
protected final Class<?> _valueType;

protected Constructor<?> _noArgsCtor;
Expand All @@ -21,10 +28,17 @@ public class BeanConstructors
*/
protected Constructor<?> _recordCtor;

/**
* Aliases for Record constructor parameters.
*
* @since 2.20
*/
protected Map<String, RecordAlias> _recordCtorAliases = new HashMap<>();

protected Constructor<?> _intCtor;

protected Constructor<?> _longCtor;
protected Constructor<?> _stringCtor;

public BeanConstructors(Class<?> valueType) {
_valueType = valueType;
}
Expand All @@ -42,6 +56,14 @@ public BeanConstructors addRecordConstructor(Constructor<?> ctor) {
return this;
}

/**
* @since 2.20
*/
public BeanConstructors addRecordConstructorAlias(String alias, Class<?> valueType, int pos) {
_recordCtorAliases.put(alias, new RecordAlias(new SimpleValueReader(valueType, 9), pos)); // TODO: Should not always be a simplevaluereader
return this;
}

public BeanConstructors addIntConstructor(Constructor<?> ctor) {
_intCtor = ctor;
return this;
Expand All @@ -57,6 +79,14 @@ public BeanConstructors addStringConstructor(Constructor<?> ctor) {
return this;
}

public RecordAlias getRecordCtorAliasValueReader(String name) {
return _recordCtorAliases.get(name);
}

public int getRecordCtorAliasesCount() {
return _recordCtorAliases.size();
}

public void forceAccess() {
if (_noArgsCtor != null) {
_noArgsCtor.setAccessible(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,17 @@ public Object readNext(JSONReader r, JsonParser p) throws IOException
}

private Object readRecord(JSONReader r, JsonParser p) throws Exception {
final Object[] values = new Object[_propsByName.size()];
final Object[] values = new Object[_propsByName.size() + _constructors.getRecordCtorAliasesCount()];

String propName;
for (; (propName = p.nextFieldName()) != null;) {
BeanConstructors.RecordAlias recAlias;
if ((recAlias = _constructors.getRecordCtorAliasValueReader(propName)) != null) {
values[recAlias.pos()] = recAlias.reader().readNext(r, p);

continue;
}

BeanPropertyReader prop = findProperty(propName);
if (prop == null) {
handleUnknown(r, p, propName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public final class RecordsHelpers {
}
private RecordsHelpers() {}

static Constructor<?> findCanonicalConstructor(Class<?> beanClass) {
public static Constructor<?> findCanonicalConstructor(Class<?> beanClass) {
// sanity check: caller shouldn't rely on it
if (!supportsRecords || !isRecordType(beanClass)) {
return null;
Expand Down Expand Up @@ -78,7 +78,7 @@ static boolean isRecordConstructor(Class<?> beanClass, Constructor<?> ctor,
}
}

static boolean isRecordType(Class<?> cls) {
public static boolean isRecordType(Class<?> cls) {
Class<?> parent = cls.getSuperclass();
return (parent != null) && "java.lang.Record".equals(parent.getName());
}
Expand Down