Skip to content

ENH: Add support for case-insensitive keys on deserialization #46

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

Merged
merged 1 commit into from
Dec 14, 2022
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.example.customer.caseinsensitive;

import io.avaje.jsonb.Json;

@Json(caseInsensitiveKeys = true)
public record ICaseContact(int id, String firstName, String lastName) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.example.customer.caseinsensitive;

import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ICaseContactTest {

Jsonb jsonb = Jsonb.builder().build();

@Test
void toJsonFromJson() {
ICaseContact bean = new ICaseContact(42, "first", "last");
String asJson = jsonb.toJson(bean);
assertThat(asJson).isEqualTo("{\"id\":42,\"firstName\":\"first\",\"lastName\":\"last\"}");

JsonType<ICaseContact> type = jsonb.type(ICaseContact.class);
ICaseContact fromJson = type.fromJson(asJson);

assertThat(fromJson).isEqualTo(bean);
assertThat(type.fromJson("{\"id\":42,\"FIRSTNAME\":\"first\",\"lastname\":\"last\"}")).isEqualTo(bean);
assertThat(type.fromJson("{\"id\":42,\"FirstName\":\"first\",\"LastName\":\"last\"}")).isEqualTo(bean);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class BeanReader {
private final TypeReader typeReader;
private final String typeProperty;
private final boolean nonAccessibleField;
private final boolean caseInsensitiveKeys;
private FieldReader unmappedField;
private boolean hasRaw;

Expand All @@ -33,7 +34,7 @@ class BeanReader {
NamingConventionReader ncReader = new NamingConventionReader(beanType);
this.namingConvention = ncReader.get();
this.typeProperty = ncReader.typeProperty();

this.caseInsensitiveKeys = ncReader.isCaseInsensitiveKeys();
this.typeReader = new TypeReader(beanType, context, namingConvention);
typeReader.process();
this.nonAccessibleField = typeReader.nonAccessibleField();
Expand All @@ -49,6 +50,7 @@ public BeanReader(TypeElement beanType, TypeElement mixInElement, ProcessingCont
final NamingConventionReader ncReader = new NamingConventionReader(beanType);
this.namingConvention = ncReader.get();
this.typeProperty = ncReader.typeProperty();
this.caseInsensitiveKeys = ncReader.isCaseInsensitiveKeys();
this.typeReader = new TypeReader(beanType, mixInElement, context, namingConvention);
typeReader.process();
this.nonAccessibleField = typeReader.nonAccessibleField();
Expand Down Expand Up @@ -354,22 +356,28 @@ private void writeFromJsonSwitch(Append writer, boolean defaultConstructor, Stri
writer.append(" reader.beginObject();").eol();
writer.append(" reader.names(names);").eol();
writer.append(" while (reader.hasNextField()) {").eol();
writer.append(" String fieldName = reader.nextField();").eol();
if (caseInsensitiveKeys) {
writer.append(" final String origFieldName = reader.nextField();").eol();
writer.append(" final String fieldName = origFieldName.toLowerCase();").eol();
} else {
writer.append(" final String fieldName = reader.nextField();").eol();
}
writer.append(" switch (fieldName) {").eol();
if (hasSubTypes) {
writer.append(" case \"%s\": {", typeProperty).eol();
writer.append(" case \"%s\": {", typePropertyKey()).eol();
writer.append(" type = stringJsonAdapter.fromJson(reader); break;").eol();
writer.append(" }").eol();
}
for (FieldReader allField : allFields) {
allField.writeFromJsonSwitch(writer, defaultConstructor, varName);
allField.writeFromJsonSwitch(writer, defaultConstructor, varName, caseInsensitiveKeys);
}
writer.append(" default: {").eol();
String unmappedFieldName = caseInsensitiveKeys ? "origFieldName" : "fieldName";
if (unmappedField != null) {
writer.append(" Object value = objectJsonAdapter.fromJson(reader);").eol();
writer.append(" unmapped.put(fieldName, value);").eol();
writer.append(" unmapped.put(%s, value);", unmappedFieldName).eol();
} else {
writer.append(" reader.unmappedField(fieldName);").eol();
writer.append(" reader.unmappedField(%s);", unmappedFieldName).eol();
writer.append(" reader.skipValue();").eol();
}
writer.append(" }").eol();
Expand All @@ -378,4 +386,8 @@ private void writeFromJsonSwitch(Append writer, boolean defaultConstructor, Stri
writer.append(" reader.endObject();").eol();
}

private String typePropertyKey() {
return caseInsensitiveKeys ? typeProperty.toLowerCase() : typeProperty;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -300,17 +300,18 @@ private String pad(String value) {
return sb.toString();
}

void writeFromJsonSwitch(Append writer, boolean defaultConstructor, String varName) {
void writeFromJsonSwitch(Append writer, boolean defaultConstructor, String varName, boolean caseInsensitiveKeys) {
if (unmapped) {
return;
}
if (aliases != null) {
for (final String alias : aliases) {
writer.append(" case \"%s\":", alias);
writer.eol();
String propertyKey = caseInsensitiveKeys ? alias.toLowerCase() : alias;
writer.append(" case \"%s\":", propertyKey).eol();
}
}
writer.append(" case \"%s\": {", propertyName).eol();
String propertyKey = caseInsensitiveKeys ? propertyName.toLowerCase() : propertyName;
writer.append(" case \"%s\": {", propertyKey).eol();
if (!deserialize) {
writer.append(" reader.skipValue();");
} else if (defaultConstructor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ class NamingConventionReader {
private static final String JSON_ANNOTATION = "io.avaje.jsonb.Json";
private static final String NAMING_ATTRIBUTE = "naming()";
private static final String TYPEPROPERTY_ATTRIBUTE = "typeProperty()";
private static final String CASEINSENSITIVEKEYS_ATTRIBUTE = "caseInsensitiveKeys()";

private String typeProperty;
private boolean caseInsensitiveKeys;
private NamingConvention namingConvention;

NamingConventionReader(TypeElement element) {
for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
if (JSON_ANNOTATION.equals(mirror.getAnnotationType().toString())) {
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : mirror.getElementValues().entrySet()) {
if (entry.getKey().toString().equals(NAMING_ATTRIBUTE)) {
String key = entry.getKey().toString();
if (key.equals(NAMING_ATTRIBUTE)) {
namingConvention = NamingConvention.of(naming(entry.getValue().toString()));
} else if (entry.getKey().toString().equals(TYPEPROPERTY_ATTRIBUTE)) {
} else if (key.equals(TYPEPROPERTY_ATTRIBUTE)) {
typeProperty = Util.trimQuotes(entry.getValue().toString());
} else if (key.equals(CASEINSENSITIVEKEYS_ATTRIBUTE)) {
caseInsensitiveKeys = Boolean.parseBoolean(entry.getValue().toString());
}
}
}
Expand All @@ -43,4 +48,8 @@ NamingConvention get() {
String typeProperty() {
return typeProperty != null ? typeProperty : "@type";
}

boolean isCaseInsensitiveKeys() {
return caseInsensitiveKeys;
}
}
5 changes: 5 additions & 0 deletions jsonb/src/main/java/io/avaje/jsonb/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
*/
String typeProperty() default "";

/**
* When set to true on deserialization keys are matched insensitive to case.
*/
boolean caseInsensitiveKeys() default false;

/**
* Specify types to generate JsonAdapters for.
* <p>
Expand Down