Skip to content

Commit 81a14e4

Browse files
authored
Merge pull request #140 from avaje/feature/support-interfaces
Support @JSON on interfaces and abstract types
2 parents 2b446c5 + 108219b commit 81a14e4

File tree

13 files changed

+321
-22
lines changed

13 files changed

+321
-22
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.example.customer.iface;
2+
3+
import io.avaje.jsonb.Json;
4+
5+
@Json
6+
public interface AIFace {
7+
8+
String one();
9+
10+
long two();
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.example.customer.iface;
2+
3+
import io.avaje.jsonb.Json;
4+
5+
@Json
6+
public interface BIFace {
7+
8+
String getOne();
9+
10+
boolean isTwo();
11+
12+
String getThree();
13+
14+
String four();
15+
16+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.example.customer.iface;
2+
3+
public interface CIFace {
4+
5+
String oneToBe();
6+
7+
long twoNotHere();
8+
9+
String hi();
10+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@Json.Import(value = CIFace.class, jsonSettings = @Json(naming = Json.Naming.LowerHyphen))
2+
package org.example.customer.iface;
3+
4+
import io.avaje.jsonb.Json;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.example.customer.iface;
2+
3+
import io.avaje.jsonb.JsonType;
4+
import io.avaje.jsonb.JsonView;
5+
import io.avaje.jsonb.Jsonb;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class AIFaceTest {
11+
12+
Jsonb jsonb = Jsonb.builder().build();
13+
14+
record Foo(String one, long two) implements AIFace {
15+
}
16+
17+
@Test
18+
void toJson() {
19+
JsonType<AIFace> type = jsonb.type(AIFace.class);
20+
21+
String asJson = type.toJson(new Foo("a", 42));
22+
assertThat(asJson).isEqualTo("{\"one\":\"a\",\"two\":42}");
23+
}
24+
25+
@Test
26+
void toJsonView() {
27+
JsonType<AIFace> type = jsonb.type(AIFace.class);
28+
29+
JsonView<AIFace> view0 = type.view("(one)");
30+
String asJson = view0.toJson(new Foo("a", 42));
31+
assertThat(asJson).isEqualTo("{\"one\":\"a\"}");
32+
33+
JsonView<AIFace> view1 = type.view("(one,two)");
34+
assertThat(view1.toJson(new Foo("a", 42))).isEqualTo("{\"one\":\"a\",\"two\":42}");
35+
36+
JsonView<AIFace> view2 = type.view("(two)");
37+
assertThat(view2.toJson(new Foo("a", 42))).isEqualTo("{\"two\":42}");
38+
}
39+
40+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.example.customer.iface;
2+
3+
import io.avaje.jsonb.JsonType;
4+
import io.avaje.jsonb.JsonView;
5+
import io.avaje.jsonb.Jsonb;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class BIFaceTest {
11+
12+
Jsonb jsonb = Jsonb.builder().build();
13+
14+
record Foo(String one, boolean two, String three, String four) implements BIFace {
15+
@Override
16+
public String getOne() {
17+
return one;
18+
}
19+
20+
@Override
21+
public boolean isTwo() {
22+
return two;
23+
}
24+
25+
@Override
26+
public String getThree() {
27+
return three;
28+
}
29+
}
30+
31+
@Test
32+
void toJson() {
33+
JsonType<BIFace> type = jsonb.type(BIFace.class);
34+
35+
String asJson = type.toJson(new Foo("a", true, "b", "c"));
36+
assertThat(asJson).isEqualTo("{\"one\":\"a\",\"two\":true,\"three\":\"b\",\"four\":\"c\"}");
37+
}
38+
39+
@Test
40+
void toJsonView() {
41+
JsonType<BIFace> type = jsonb.type(BIFace.class);
42+
43+
JsonView<BIFace> view0 = type.view("(one)");
44+
String asJson = view0.toJson(new Foo("a", true, "b", "c"));
45+
assertThat(asJson).isEqualTo("{\"one\":\"a\"}");
46+
47+
JsonView<BIFace> view1 = type.view("(one,two,four)");
48+
assertThat(view1.toJson(new Foo("a", true, "b", "c"))).isEqualTo("{\"one\":\"a\",\"two\":true,\"four\":\"c\"}");
49+
50+
JsonView<BIFace> view2 = type.view("(two)");
51+
assertThat(view2.toJson(new Foo("a", true, "b", "c"))).isEqualTo("{\"two\":true}");
52+
}
53+
54+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.example.customer.iface;
2+
3+
import io.avaje.jsonb.JsonType;
4+
import io.avaje.jsonb.JsonView;
5+
import io.avaje.jsonb.Jsonb;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class CIFaceTest {
11+
12+
Jsonb jsonb = Jsonb.builder().build();
13+
14+
record Foo(String oneToBe, long twoNotHere, String hi) implements CIFace {
15+
}
16+
17+
@Test
18+
void toJson() {
19+
JsonType<CIFace> type = jsonb.type(CIFace.class);
20+
21+
String asJson = type.toJson(new Foo("a", 42, "b"));
22+
assertThat(asJson).isEqualTo("{\"one-to-be\":\"a\",\"two-not-here\":42,\"hi\":\"b\"}");
23+
}
24+
25+
@Test
26+
void toJsonView() {
27+
JsonType<CIFace> type = jsonb.type(CIFace.class);
28+
29+
JsonView<CIFace> view0 = type.view("(one-to-be)");
30+
String asJson = view0.toJson(new Foo("a", 42, "b"));
31+
assertThat(asJson).isEqualTo("{\"one-to-be\":\"a\"}");
32+
33+
JsonView<CIFace> view1 = type.view("(one-to-be,two-not-here)");
34+
assertThat(view1.toJson(new Foo("a", 42, "b"))).isEqualTo("{\"one-to-be\":\"a\",\"two-not-here\":42}");
35+
36+
JsonView<CIFace> view2 = type.view("(two-not-here)");
37+
assertThat(view2.toJson(new Foo("a", 42, "b"))).isEqualTo("{\"two-not-here\":42}");
38+
}
39+
40+
}

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

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ final class ClassReader implements BeanReader {
2020

2121
private final MethodReader constructor;
2222
private final List<FieldReader> allFields;
23+
private final List<MethodProperty> methodProperties;
2324
private final Set<String> importTypes = new TreeSet<>();
2425
private final NamingConvention namingConvention;
2526
private final boolean hasSubTypes;
2627
private final TypeReader typeReader;
2728
private final String typeProperty;
2829
private final boolean nonAccessibleField;
2930
private final boolean caseInsensitiveKeys;
31+
private final boolean readOnlyInterface;
3032
private FieldReader unmappedField;
3133
private boolean hasRaw;
3234
private final boolean isRecord;
@@ -37,6 +39,7 @@ final class ClassReader implements BeanReader {
3739
private final Map<String, Integer> frequencyMap = new HashMap<>();
3840
private final Map<String, Boolean> isCommonFieldMap = new HashMap<>();
3941
private final boolean optional;
42+
private final List<TypeSubTypeMeta> subTypes;
4043

4144
ClassReader(TypeElement beanType) {
4245
this(beanType, null);
@@ -58,7 +61,11 @@ final class ClassReader implements BeanReader {
5861
this.constructor = typeReader.constructor();
5962
this.optional = typeReader.hasOptional();
6063
this.isRecord = isRecord(beanType);
61-
typeReader.subTypes().stream().map(TypeSubTypeMeta::type).forEach(importTypes::add);
64+
this.subTypes = typeReader.subTypes();
65+
this.readOnlyInterface = allFields.isEmpty() && subTypes.isEmpty();
66+
this.methodProperties = typeReader.methodProperties();
67+
68+
subTypes.stream().map(TypeSubTypeMeta::type).forEach(importTypes::add);
6269

6370
final var userTypeField = allFields.stream().filter(f -> f.propertyName().equals(typePropertyKey())).findAny();
6471

@@ -132,6 +139,9 @@ public void read() {
132139
unmappedField = field;
133140
}
134141
}
142+
for (final MethodProperty methodProperty : methodProperties) {
143+
methodProperty.addImports(importTypes);
144+
}
135145
}
136146

137147
private Set<String> importTypes() {
@@ -151,6 +161,9 @@ private Set<String> importTypes() {
151161
for (final FieldReader allField : allFields) {
152162
allField.addImports(importTypes);
153163
}
164+
for (final MethodProperty methodProperty : methodProperties) {
165+
methodProperty.addImports(importTypes);
166+
}
154167
return importTypes;
155168
}
156169

@@ -194,6 +207,11 @@ public void writeFields(Append writer) {
194207
allField.writeField(writer);
195208
}
196209
}
210+
for (final MethodProperty methodProperty : methodProperties) {
211+
if (uniqueTypes.add(methodProperty.adapterShortType())) {
212+
methodProperty.writeField(writer);
213+
}
214+
}
197215
writer.append(" private final PropertyNames names;").eol();
198216
writer.eol();
199217
}
@@ -221,12 +239,25 @@ public void writeConstructor(Append writer) {
221239
allField.writeConstructor(writer);
222240
}
223241
}
242+
for (MethodProperty methodProperty : methodProperties) {
243+
if (uniqueTypes.add(methodProperty.adapterShortType())) {
244+
methodProperty.writeConstructor(writer);
245+
}
246+
}
224247
writer.append(" this.names = jsonb.properties(");
225248
if (hasSubTypes) {
226249
writer.append("\"").append(typeProperty).append("\", ");
227250
}
228-
final StringBuilder builder = new StringBuilder();
251+
writer.append(propertyNames());
252+
writer.append(");").eol();
253+
}
254+
255+
private String propertyNames() {
256+
return readOnlyInterface ? propertyNamesReadOnly() : propertyNamesFields();
257+
}
229258

259+
private String propertyNamesFields() {
260+
final StringBuilder builder = new StringBuilder();
230261
//set to prevent writing same key twice
231262
final var seen = new HashSet<String>();
232263
for (int i = 0, size = allFields.size(); i < size; i++) {
@@ -243,8 +274,18 @@ public void writeConstructor(Append writer) {
243274
}
244275
builder.append("\"").append(fieldReader.propertyName()).append("\"");
245276
}
246-
writer.append(builder.toString().replace(" , ", ""));
247-
writer.append(");").eol();
277+
return builder.toString().replace(" , ", "");
278+
}
279+
280+
private String propertyNamesReadOnly() {
281+
final StringBuilder builder = new StringBuilder();
282+
for (int i = 0; i < methodProperties.size(); i++) {
283+
if (i > 0) {
284+
builder.append(", ");
285+
}
286+
builder.append("\"").append(methodProperties.get(i).propertyName()).append("\"");
287+
}
288+
return builder.toString().replace(" , ", "");
248289
}
249290

250291
@Override
@@ -271,13 +312,14 @@ private void writeViewBuild(Append writer) {
271312
writer.append(" @Override").eol();
272313
writer.append(" public void build(ViewBuilder builder, String name, MethodHandle handle) {").eol();
273314
writer.append(" builder.beginObject(name, handle);").eol();
274-
if (!hasSubTypes) {
275-
for (final FieldReader allField : allFields) {
276-
if (allField.includeToJson(null)) {
277-
allField.writeViewBuilder(writer, shortName);
278-
}
315+
for (final FieldReader allField : allFields) {
316+
if (allField.includeToJson(null)) {
317+
allField.writeViewBuilder(writer, shortName);
279318
}
280319
}
320+
for (final MethodProperty methodProperty : methodProperties) {
321+
methodProperty.writeViewBuilder(writer, shortName);
322+
}
281323
writer.append(" builder.endObject();").eol();
282324
writer.append(" }").eol();
283325
}
@@ -300,7 +342,6 @@ public void writeToJson(Append writer) {
300342

301343
private void writeToJsonForSubtypes(Append writer, String varName) {
302344
if (hasSubTypes) {
303-
final List<TypeSubTypeMeta> subTypes = typeReader.subTypes();
304345
for (int i = 0; i < subTypes.size(); i++) {
305346
final TypeSubTypeMeta subTypeMeta = subTypes.get(i);
306347
final String subType = subTypeMeta.type();
@@ -330,6 +371,9 @@ private void writeToJsonForType(Append writer, String varName, String prefix, St
330371
allField.writeToJson(writer, varName, prefix);
331372
}
332373
}
374+
for (final MethodProperty methodProperty : methodProperties) {
375+
methodProperty.writeToJson(writer, varName, prefix);
376+
}
333377
}
334378

335379
@Override
@@ -338,6 +382,12 @@ public void writeFromJson(Append writer) {
338382
writer.eol();
339383
writer.append(" @Override").eol();
340384
writer.append(" public %s fromJson(JsonReader reader) {", shortName, varName).eol();
385+
if (readOnlyInterface) {
386+
writer.append(" throw new UnsupportedOperationException();").eol();
387+
writer.append(" }").eol();
388+
return;
389+
}
390+
341391
final boolean directLoad = (constructor == null && !hasSubTypes && !optional);
342392
if (directLoad) {
343393
// default public constructor
@@ -401,7 +451,7 @@ private void writeJsonBuildResult(Append writer, String varName) {
401451

402452
private void writeFromJsonWithSubTypes(Append writer) {
403453
final var typeVar = usesTypeProperty ? "_val$" + typePropertyKey() : "type";
404-
final var useSwitch = typeReader.subTypes().size() >= 3;
454+
final var useSwitch = subTypes.size() >= 3;
405455

406456
if (!useSwitch || !nullSwitch) {
407457
writer.append(" if (%s == null) {", typeVar).eol();
@@ -422,7 +472,7 @@ private void writeFromJsonWithSubTypes(Append writer) {
422472
final Map<String, Integer> frequencyMap2 = new HashMap<>();
423473
final var req = new SubTypeRequest(typeVar, this, useSwitch, useEnum, frequencyMap2, isCommonFieldMap);
424474

425-
for (final TypeSubTypeMeta subTypeMeta : typeReader.subTypes()) {
475+
for (final TypeSubTypeMeta subTypeMeta : subTypes) {
426476
final var varName = Util.initLower(Util.shortName(subTypeMeta.type()));
427477
subTypeMeta.writeFromJsonBuild(writer, varName, req);
428478
}

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

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

33
import javax.lang.model.type.TypeMirror;
4+
import java.util.ArrayList;
45
import java.util.List;
56
import java.util.Set;
67

@@ -25,6 +26,10 @@ final class FieldProperty {
2526
private MethodReader getter;
2627
private MethodReader setter;
2728

29+
FieldProperty(MethodReader methodReader) {
30+
this(methodReader.returnType(), false, false, new ArrayList<>(), false, methodReader.getName());
31+
}
32+
2833
FieldProperty(TypeMirror asType, boolean raw, boolean unmapped, List<String> genericTypeParams,
2934
boolean publicField, String fieldName) {
3035
this.raw = raw;

0 commit comments

Comments
 (0)