diff --git a/blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java b/blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java index 08204631..60920ebe 100644 --- a/blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java +++ b/blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java @@ -9,6 +9,9 @@ public class MyGenericHolder { String author; T document; + @Json + public record MyGenericHolderRecord(T title, T2 author, T3 document) {} + public String getTitle() { return title; } diff --git a/blackbox-test/src/test/java/org/example/customer/generics/MyGenericHolderRecordTest.java b/blackbox-test/src/test/java/org/example/customer/generics/MyGenericHolderRecordTest.java new file mode 100644 index 00000000..c678fd77 --- /dev/null +++ b/blackbox-test/src/test/java/org/example/customer/generics/MyGenericHolderRecordTest.java @@ -0,0 +1,81 @@ +package org.example.customer.generics; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.LinkedHashMap; + +import org.example.customer.Address; +import org.example.customer.generics.MyGenericHolder.MyGenericHolderRecord; +import org.junit.jupiter.api.Test; + +import io.avaje.jsonb.JsonType; +import io.avaje.jsonb.Jsonb; +import io.avaje.jsonb.Types; + +class MyGenericHolderRecordTest { + + Jsonb jsonb = Jsonb.builder().build(); + + private static MyGenericHolderRecord createTestData() { + return new MyGenericHolderRecord<>("hello", "art", new Address(90L, "one")); + } + + @SuppressWarnings({"rawtypes"}) + @Test + void toJson() { + final var bean = createTestData(); + + final var type = jsonb.type(MyGenericHolderRecord.class); + + final var asJson = type.toJson(bean); + assertThat(asJson) + .isEqualTo( + "{\"title\":\"hello\",\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}"); + assertThat(jsonb.toJson(bean)).isEqualTo(asJson); + + final var pageResult = type.fromJson(asJson); + final var document = pageResult.document(); + // reading via Object means the list contains LinkedHashMap + assertThat(document).isInstanceOf(LinkedHashMap.class); + final var asMap = (LinkedHashMap) document; + assertThat(asMap.get("street")).isEqualTo("one"); + + final var view = type.view("author,document(id)"); + final var partialJson2 = view.toJson(bean); + // not supporting partial on the generic object (output includes street) + assertThat(partialJson2) + .isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}"); + } + + @Test + void toJson_withGenericParam() { + final var bean = createTestData(); + + final var jsonb = Jsonb.builder().build(); + final JsonType> type = + jsonb.type( + Types.newParameterizedType( + MyGenericHolderRecord.class, String.class, String.class, Address.class)); + + final var asJson = type.toJson(bean); + assertThat(asJson) + .isEqualTo( + "{\"title\":\"hello\",\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}"); + assertThat(jsonb.toJson(bean)).isEqualTo(asJson); + + final var genericResult = type.fromJson(asJson); + final var document = genericResult.document(); + + assertThat(document.getId()).isEqualTo(90L); + assertThat(document.getStreet()).isEqualTo("one"); + + final var partial = type.view("author,document(*)"); + final var partialJson = partial.toJson(bean); + assertThat(partialJson) + .isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}"); + + final var partial2 = type.view("author,document(id)"); + final var partialJson2 = partial2.toJson(bean); + assertThat(partialJson2).isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90}}"); + } +} diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java index 14a31897..d03ab4d7 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java @@ -1,14 +1,16 @@ package io.avaje.jsonb.generator; -import io.avaje.jsonb.Json; - -import javax.lang.model.element.Element; -import javax.lang.model.element.TypeElement; +import java.lang.reflect.InvocationTargetException; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import io.avaje.jsonb.Json; + class BeanReader { private final TypeElement beanType; @@ -25,6 +27,7 @@ class BeanReader { private final boolean nonAccessibleField; private FieldReader unmappedField; private boolean hasRaw; + private final boolean isRecord; BeanReader(TypeElement beanType, ProcessingContext context) { this.beanType = beanType; @@ -40,6 +43,7 @@ class BeanReader { this.hasSubTypes = typeReader.hasSubTypes(); this.allFields = typeReader.allFields(); this.constructor = typeReader.constructor(); + this.isRecord = isRecord(beanType); } public BeanReader(TypeElement beanType, TypeElement mixInElement, ProcessingContext context) { @@ -55,6 +59,21 @@ public BeanReader(TypeElement beanType, TypeElement mixInElement, ProcessingCont this.hasSubTypes = typeReader.hasSubTypes(); this.allFields = typeReader.allFields(); this.constructor = typeReader.constructor(); + this.isRecord = isRecord(beanType); + } + + boolean isRecord(TypeElement beanType) { + try { + final List recordComponents = + (List) + TypeElement.class.getMethod("getRecordComponents").invoke(beanType); + return !recordComponents.isEmpty(); + } catch (IllegalAccessException + | InvocationTargetException + | NoSuchMethodException + | SecurityException e) { + return false; + } } int genericTypeParamsCount() { @@ -279,9 +298,14 @@ void writeFromJson(Append writer) { // default public constructor writer.append(" %s _$%s = new %s();", shortName, varName, shortName).eol(); } else { - writer.append(" // variables to read json values into, constructor params don't need _set$ flags").eol(); - for (FieldReader allField : allFields) { - if (allField.includeFromJson()) { + writer + .append( + " // variables to read json values into, constructor params don't need _set$ flags") + .eol(); + for (final FieldReader allField : allFields) { + if (isRecord) { + allField.writeFromJsonVariablesRecord(writer); + } else if (allField.includeFromJson()) { allField.writeFromJsonVariables(writer); } } diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java index f603ca10..568dce4d 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java @@ -84,10 +84,7 @@ private String initAdapterShortType(String shortType) { private String initShortName() { if (genericTypeParameter) { - String name = genericType.shortName(); - for (String typeParam : genericTypeParams) { - name = name.replace(typeParam, ""); - } + final String name = genericType.shortName(); return Util.initLower(name) + "JsonAdapterGeneric"; } return Util.initLower(genericType.shortName()) + "JsonAdapter"; @@ -288,6 +285,13 @@ void writeFromJsonVariables(Append writer) { writer.eol(); } + void writeFromJsonVariablesRecord(Append writer) { + final String type = genericTypeParameter ? "Object" : genericType.shortType(); + writer.append(" %s _val$%s = %s;", pad(type), fieldName, defaultValue); + + writer.eol(); + } + private String pad(String value) { final int pad = 10 - value.length(); if (pad < 1) { diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java index 3e414229..75c81688 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java @@ -53,8 +53,14 @@ void write() throws IOException { private void writeFactory() { if (genericParamsCount > 0) { + + String typeName = adapterShortName; + int nestedIndex = adapterShortName.indexOf("$"); + if (nestedIndex != -1) { + typeName = typeName.substring(nestedIndex + 1); + } writer.append(" public static final JsonAdapter.Factory Factory = (type, jsonb) -> {").eol(); - writer.append(" if (type instanceof ParameterizedType && Types.rawType(type) == %s.class) {", adapterShortName).eol(); + writer.append(" if (type instanceof ParameterizedType && Types.rawType(type) == %s.class) {", typeName).eol(); writer.append(" Type[] args = Types.typeArguments(type);").eol(); writer.append(" return new %sJsonAdapter(jsonb", adapterShortName); for (int i = 0; i < genericParamsCount; i++) { diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java index c2c64a00..98ca28ec 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java @@ -53,11 +53,11 @@ void writeMetaInf() throws IOException { private void writeRegister() { writer.append(" @Override").eol(); writer.append(" public void register(Jsonb.Builder builder) {").eol(); - List strings = metaData.allFactories(); - for (String adapterFullName : strings) { - String adapterShortName = Util.shortName(adapterFullName); - String typeName = typeShortName(adapterShortName); - writer.append(" builder.add(%sJsonAdapter.Factory);", typeName).eol(); + final List strings = metaData.allFactories(); + for (final String adapterFullName : strings) { + final String adapterShortName = Util.shortName(adapterFullName); + + writer.append(" builder.add(%s.Factory);", adapterShortName).eol(); } for (String adapterFullName : metaData.all()) { String adapterShortName = Util.shortName(adapterFullName); diff --git a/jsonb/src/main/java/io/avaje/jsonb/core/Util.java b/jsonb/src/main/java/io/avaje/jsonb/core/Util.java index bd6ebe4a..640b9ca0 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/Util.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/Util.java @@ -286,13 +286,13 @@ static final class ParameterizedTypeImpl implements ParameterizedType { public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) { // Require an owner type if the raw type needs it. - if (rawType instanceof Class) { + if (ownerType != null && rawType instanceof Class) { Class enclosingClass = ((Class) rawType).getEnclosingClass(); - if (ownerType != null) { - if (enclosingClass == null || Util.rawType(ownerType) != enclosingClass) { - throw new IllegalArgumentException( + + if (enclosingClass == null || Util.rawType(ownerType) != enclosingClass) { + throw new IllegalArgumentException( "unexpected owner type for " + rawType + ": " + ownerType); - } + } else if (enclosingClass != null) { throw new IllegalArgumentException("unexpected owner type for " + rawType + ": null"); }