Skip to content

Commit cc19f2a

Browse files
committed
#25 - Detect "value types" that should have their own custom JsonAdapter
1 parent 00dbf0d commit cc19f2a

File tree

13 files changed

+208
-11
lines changed

13 files changed

+208
-11
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.example.customer.customtype;
2+
3+
4+
import io.avaje.jsonb.*;
5+
6+
/**
7+
* Register via service loading.
8+
*/
9+
public class CustomTypeComponent implements JsonbComponent {
10+
11+
@Override
12+
public void register(Jsonb.Builder builder) {
13+
builder.add(MyCustomScalarType.class, new CustomTypeAdapterWithStar().nullSafe());
14+
}
15+
16+
static class CustomTypeAdapterWithStar extends JsonAdapter<MyCustomScalarType> {
17+
18+
@Override
19+
public void toJson(JsonWriter writer, MyCustomScalarType value) {
20+
writer.value("*** " + value.toString());
21+
}
22+
23+
@Override
24+
public MyCustomScalarType fromJson(JsonReader reader) {
25+
String encoded = reader.readString();
26+
return MyCustomScalarType.of(encoded.substring(4)); // trim those stars
27+
}
28+
}
29+
30+
}
31+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.example.customer.customtype;
2+
3+
import java.util.Arrays;
4+
import java.util.Base64;
5+
6+
public final class MyCustomScalarType {
7+
8+
private final byte[] data;
9+
10+
public MyCustomScalarType(byte[] data) {
11+
this.data = data;
12+
}
13+
14+
public static MyCustomScalarType of(String encoded) {
15+
byte[] bytes = Base64.getDecoder().decode(encoded);
16+
return new MyCustomScalarType(bytes);
17+
}
18+
19+
public static MyCustomScalarType of(byte[] raw) {
20+
return new MyCustomScalarType(raw);
21+
}
22+
23+
@Override
24+
public String toString() {
25+
return Base64.getEncoder().encodeToString(data);
26+
}
27+
28+
@Override
29+
public boolean equals(Object o) {
30+
if (this == o) return true;
31+
if (o == null || getClass() != o.getClass()) return false;
32+
MyCustomScalarType that = (MyCustomScalarType) o;
33+
return Arrays.equals(data, that.data);
34+
}
35+
36+
@Override
37+
public int hashCode() {
38+
return Arrays.hashCode(data);
39+
}
40+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.example.customer.customtype;
2+
3+
import io.avaje.jsonb.Json;
4+
5+
@Json
6+
public record MyWrapper(int id, String base, MyCustomScalarType custom) {
7+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.example.customer.customtype.CustomTypeComponent
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.example.customer.customtype;
2+
3+
import io.avaje.jsonb.JsonAdapter;
4+
import io.avaje.jsonb.JsonReader;
5+
import io.avaje.jsonb.JsonWriter;
6+
import io.avaje.jsonb.Jsonb;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.nio.charset.StandardCharsets;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class CustomScalarTypeTest {
14+
15+
/**
16+
* Using the {@link CustomTypeComponent} which is service loaded.
17+
*/
18+
@Test
19+
void toJson_fromJson_usingComponent() {
20+
Jsonb jsonb = Jsonb.builder()
21+
// service loading finds and loads CustomTypeComponent
22+
.build();
23+
24+
MyWrapper wrapper = new MyWrapper(42, "hello", new MyCustomScalarType("hello".getBytes(StandardCharsets.UTF_8)));
25+
26+
String asJson = jsonb.toJson(wrapper);
27+
assertThat(asJson).isEqualTo("{\"id\":42,\"base\":\"hello\",\"custom\":\"*** aGVsbG8=\"}");
28+
29+
MyWrapper wrapper1 = jsonb.type(MyWrapper.class).fromJson(asJson);
30+
31+
assertThat(wrapper1).isEqualTo(wrapper);
32+
assertThat(wrapper1.custom()).isEqualTo(wrapper.custom());
33+
}
34+
35+
@Test
36+
void toJson_fromJson() {
37+
Jsonb jsonb = Jsonb.builder()
38+
// explicitly register the CustomTypeAdapter for our value type
39+
.add(MyCustomScalarType.class, new CustomTypeAdapter().nullSafe())
40+
.build();
41+
42+
MyWrapper wrapper = new MyWrapper(42, "hello", new MyCustomScalarType("hello".getBytes(StandardCharsets.UTF_8)));
43+
44+
String asJson = jsonb.toJson(wrapper);
45+
assertThat(asJson).isEqualTo("{\"id\":42,\"base\":\"hello\",\"custom\":\"aGVsbG8=\"}");
46+
47+
MyWrapper wrapper1 = jsonb.type(MyWrapper.class).fromJson(asJson);
48+
49+
assertThat(wrapper1).isEqualTo(wrapper);
50+
assertThat(wrapper1.custom()).isEqualTo(wrapper.custom());
51+
}
52+
53+
static class CustomTypeAdapter extends JsonAdapter<MyCustomScalarType> {
54+
55+
@Override
56+
public void toJson(JsonWriter writer, MyCustomScalarType value) {
57+
writer.value(value.toString());
58+
}
59+
60+
@Override
61+
public MyCustomScalarType fromJson(JsonReader reader) {
62+
String encoded = reader.readString();
63+
return MyCustomScalarType.of(encoded);
64+
}
65+
}
66+
67+
}

jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.avaje.jsonb.generator;
22

3+
import io.avaje.jsonb.Json;
4+
35
import javax.lang.model.element.Element;
46
import javax.lang.model.element.TypeElement;
57
import java.util.HashSet;
@@ -20,6 +22,7 @@ class BeanReader {
2022
private final boolean hasSubTypes;
2123
private final TypeReader typeReader;
2224
private final String typeProperty;
25+
private final boolean nonAccessibleField;
2326
private FieldReader unmappedField;
2427
private boolean hasRaw;
2528

@@ -33,6 +36,7 @@ class BeanReader {
3336

3437
this.typeReader = new TypeReader(beanType, context, namingConvention);
3538
typeReader.process();
39+
this.nonAccessibleField = typeReader.nonAccessibleField();
3640
this.hasSubTypes = typeReader.hasSubTypes();
3741
this.allFields = typeReader.allFields();
3842
this.constructor = typeReader.constructor();
@@ -59,6 +63,14 @@ boolean hasSubtypes() {
5963
return hasSubTypes;
6064
}
6165

66+
boolean nonAccessibleField() {
67+
return nonAccessibleField;
68+
}
69+
70+
boolean hasJsonAnnotation() {
71+
return beanType.getAnnotation(Json.class) != null;
72+
}
73+
6274
void read() {
6375
for (FieldReader field : allFields) {
6476
field.addImports(importTypes);

jsonb-generator/src/main/java/io/avaje/jsonb/generator/Processor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ private void writeAdapters(Set<? extends Element> beans) {
160160
private void writeAdapterForType(TypeElement typeElement) {
161161
BeanReader beanReader = new BeanReader(typeElement, context);
162162
beanReader.read();
163+
if (beanReader.nonAccessibleField()) {
164+
if (beanReader.hasJsonAnnotation()) {
165+
context.logError("Error JsonAdapter due to nonAccessibleField for %s ", beanReader);
166+
}
167+
return;
168+
}
163169
try {
164170
SimpleAdapterWriter beanWriter = new SimpleAdapterWriter(beanReader, context);
165171
metaData.add(beanWriter.fullName());

jsonb-generator/src/main/java/io/avaje/jsonb/generator/TypeReader.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.avaje.jsonb.generator;
22

3+
import io.avaje.jsonb.Json;
4+
35
import javax.lang.model.element.*;
46
import java.util.*;
57

@@ -23,15 +25,18 @@ class TypeReader {
2325
private final TypeElement baseType;
2426
private final ProcessingContext context;
2527
private final NamingConvention namingConvention;
28+
private final boolean hasJsonAnnotation;
2629
private MethodReader constructor;
2730
private boolean defaultPublicConstructor;
2831
private final Map<String, MethodReader.MethodParam> constructorParamMap = new LinkedHashMap<>();
2932
private TypeSubTypeMeta currentSubType;
33+
private boolean nonAccessibleField;
3034

3135
TypeReader(TypeElement baseType, ProcessingContext context, NamingConvention namingConvention) {
3236
this.baseType = baseType;
3337
this.context = context;
3438
this.namingConvention = namingConvention;
39+
this.hasJsonAnnotation = baseType.getAnnotation(Json.class) != null;
3540
this.subTypes = new TypeSubTypeReader(baseType, context);
3641
}
3742

@@ -192,7 +197,12 @@ private void matchFieldToGetter(FieldReader field) {
192197
if (!matchFieldToGetter2(field, false)) {
193198
if (!matchFieldToGetter2(field, true)) {
194199
if (!field.isPublicField()) {
195-
context.logError("Non public field " + baseType + " " + field.fieldName() + " with no matching getter ?");
200+
nonAccessibleField = true;
201+
if (hasJsonAnnotation) {
202+
context.logError("Non accessible field " + baseType + " " + field.fieldName() + " with no matching getter?");
203+
} else {
204+
context.logDebug("Non accessible field " + baseType + " " + field.fieldName());
205+
}
196206
}
197207
}
198208
}
@@ -238,6 +248,10 @@ private String isGetterName(String name) {
238248
return "is" + Util.initCap(name);
239249
}
240250

251+
boolean nonAccessibleField() {
252+
return nonAccessibleField;
253+
}
254+
241255
List<FieldReader> allFields() {
242256
return allFields;
243257
}

jsonb-jakarta/src/test/java/org/example/MyCustomerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class MyCustomerTest {
1515
@Test
1616
void toJson_fromJson() {
1717

18-
Jsonb jsonb = Jsonb.newBuilder().build();
18+
Jsonb jsonb = Jsonb.builder().build();
1919

2020
MyCustomer myCustomer = new MyCustomer(42, "rob", "foo");
2121
JsonType<MyCustomer> type = jsonb.type(MyCustomer.class);
@@ -36,7 +36,7 @@ void toJson_fromJson() {
3636
@Test
3737
void list_toJson_fromJson() {
3838

39-
Jsonb jsonb = Jsonb.newBuilder().adapter(new JakartaIOAdapter()).build();
39+
Jsonb jsonb = Jsonb.builder().adapter(new JakartaIOAdapter()).build();
4040

4141
List<MyCustomer> customers = new ArrayList<>();
4242
customers.add(new MyCustomer(42, "rob", "foo"));

jsonb/src/main/java/io/avaje/jsonb/Jsonb.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ interface Builder {
396396
/**
397397
* Add a Component which can provide multiple JsonAdapters and or configuration.
398398
*/
399-
Builder add(Jsonb.Component component);
399+
Builder add(JsonbComponent component);
400400

401401
/**
402402
* Add a JsonAdapter.Factory which provides JsonAdapters to use.
@@ -425,7 +425,7 @@ interface AdapterBuilder {
425425
* Components register JsonAdapters Jsonb.Builder
426426
*/
427427
@FunctionalInterface
428-
interface Component {
428+
interface Component extends JsonbComponent {
429429

430430
/**
431431
* Register JsonAdapters with the Builder.

0 commit comments

Comments
 (0)