From 49eb58e056a8f5ff93fe8e766d416733a43ac917 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 21 Jul 2023 16:52:52 +0200 Subject: [PATCH] Support builds in Micronaut Serialization --- ...micronaut.build.internal.serde-base.gradle | 1 + gradle.properties | 2 +- gradle/libs.versions.toml | 4 +- .../JsonSerializeDeserializeSpec.groovy | 70 ++++++- .../serde/jackson/builder/BuilderSpec.groovy | 29 +++ .../serde/jackson/builder/TestBuildMe.java | 47 +++++ .../databind/JsonDeserializeMapper.java | 25 ++- .../support/deserializers/DeserBean.java | 196 +++++++++++------- .../deserializers/ObjectDeserializer.java | 9 + .../SimpleBuilderDeserializer.java | 98 +++++++++ 10 files changed, 403 insertions(+), 78 deletions(-) create mode 100644 serde-jackson/src/test/groovy/io/micronaut/serde/jackson/builder/BuilderSpec.groovy create mode 100644 serde-jackson/src/test/java/io/micronaut/serde/jackson/builder/TestBuildMe.java create mode 100644 serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleBuilderDeserializer.java diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.serde-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.serde-base.gradle index b965e9bb6..e69b970fb 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.serde-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.serde-base.gradle @@ -1,3 +1,4 @@ repositories { mavenCentral() + mavenLocal() } diff --git a/gradle.properties b/gradle.properties index bb7795b72..bb54f80c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=2.0.3-SNAPSHOT +projectVersion=2.1.0-SNAPSHOT projectGroup=io.micronaut.serde jsonbApi=https://jakarta.ee/specifications/jsonb/2.0/apidocs/jakarta/json/bind/annotation diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d2e7b2310..31f404b4d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -micronaut = "4.0.0" -micronaut-platform = "4.0.0-RC1" +micronaut = "4.1.0-SNAPSHOT" +micronaut-platform = "4.0.0" micronaut-docs = "2.0.0" micronaut-gradle-plugin = "4.0.1" micronaut-test = "4.0.0" diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonSerializeDeserializeSpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonSerializeDeserializeSpec.groovy index 88e018027..6a122a404 100644 --- a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonSerializeDeserializeSpec.groovy +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonSerializeDeserializeSpec.groovy @@ -4,6 +4,70 @@ import io.micronaut.serde.jackson.JsonCompileSpec class JsonSerializeDeserializeSpec extends JsonCompileSpec { + void 'test json deserialize builder'() { + given: + def context = buildContext('test.TestBuildMe', """ +package test; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.micronaut.serde.annotation.Serdeable; + +@JsonDeserialize(builder = TestBuildMe.Builder.class) +class TestBuildMe { + private final String name; + private final int age; + + private TestBuildMe(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public static final class Builder { + private String name; + private int age; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder age(int age) { + this.age = age; + return this; + } + + public TestBuildMe build() { + return new TestBuildMe( + name, + age + ); + } + } +} +""") + + when: + def result = jsonMapper.readValue('{"name":"Fred", "age": 30}', typeUnderTest) + + then: + result.name == 'Fred' + result.age == '30' + + when: + def json = writeJson(jsonMapper, result) + + then: + json == '{"name":"Fred", "age": 30}' + } + void 'test json serialize/deserialize as'() { given: def context = buildContext('test.Test', """ @@ -25,7 +89,7 @@ class TestImpl implements Test { TestImpl(String value) { this.value = value; } - + @Override public String getValue() { return value; @@ -67,7 +131,7 @@ class ClientAuthentication extends ServerAuthentication implements Authenticatio ClientAuthentication(String value) { super(value); } - + public String getAnother() { return "Shouldn't appear in serialization output"; } @@ -79,7 +143,7 @@ class ServerAuthentication implements Authentication { ServerAuthentication(String value) { this.value = value; } - + @Override public String getValue() { return value; diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/builder/BuilderSpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/builder/BuilderSpec.groovy new file mode 100644 index 000000000..bbceadfce --- /dev/null +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/builder/BuilderSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.serde.jackson.builder + +import io.micronaut.serde.ObjectMapper +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest +class BuilderSpec extends Specification { + @Inject ObjectMapper objectMapper + + void "test serialize/deserialize builder"() { + given: + def json = '{"name":"Fred","age":30}' + + when: + def value = objectMapper.readValue(json, TestBuildMe) + + then: + value.name == 'Fred' + value.age == 30 + + when: + def result = objectMapper.writeValueAsString(value) + + then: + result == json + } +} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/builder/TestBuildMe.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/builder/TestBuildMe.java new file mode 100644 index 000000000..df4eff7a8 --- /dev/null +++ b/serde-jackson/src/test/java/io/micronaut/serde/jackson/builder/TestBuildMe.java @@ -0,0 +1,47 @@ +package io.micronaut.serde.jackson.builder; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.micronaut.core.annotation.Introspected; + +@JsonDeserialize( + builder = TestBuildMe.Builder.class +) +public class TestBuildMe { + private final String name; + private final int age; + + private TestBuildMe(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public static final class Builder { + private String name; + private int age; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder age(int age) { + this.age = age; + return this; + } + + public TestBuildMe build() { + return new TestBuildMe( + name, + age + ); + } + } +} diff --git a/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/databind/JsonDeserializeMapper.java b/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/databind/JsonDeserializeMapper.java index 1e784e0b8..67697cf62 100644 --- a/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/databind/JsonDeserializeMapper.java +++ b/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/databind/JsonDeserializeMapper.java @@ -17,12 +17,14 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Set; +import io.micronaut.core.annotation.AccessorsStyle; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.Introspected; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.serde.config.annotation.SerdeConfig; import io.micronaut.serde.processor.jackson.ValidatingAnnotationMapper; @@ -42,12 +44,31 @@ protected List> mapValid(AnnotationValue annotati .build() ); } + AnnotationClassValue builderClass = annotation.annotationClassValue("builder").orElse(null); + if (builderClass != null) { + AnnotationValueBuilder builderDef = AnnotationValue.builder(Introspected.IntrospectionBuilder.class); + builderDef.member("builderClass", builderClass); + visitorContext.getClassElement(builderClass.getName()).ifPresent(t -> { + AnnotationValue jsonPojoAnn = t.getAnnotation("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder"); + if (jsonPojoAnn != null) { + jsonPojoAnn.stringValue("buildMethodName").ifPresent(n -> builderDef.member("creatorMethod", n)); + jsonPojoAnn.stringValue("withPrefix").ifPresent(n -> + builderDef.member("accessorStyle", AnnotationValue.builder(AccessorsStyle.class).member("writePrefixes", n).build()) + ); + } + }); + annotations.add( + AnnotationValue.builder(Introspected.class) + .member("builder", builderDef.build()) + .build() + ); + } return annotations; } @Override protected Set getSupportedMemberNames() { - return Collections.singleton("as"); + return Set.of("as", "builder"); } @Override diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java index 86da21371..26df688db 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java @@ -84,6 +84,8 @@ class DeserBean { public final boolean delegating; public final boolean simpleBean; public final boolean recordLikeBean; + + public final boolean hasBuilder; public final ConversionService conversionService; private volatile boolean initialized; @@ -103,7 +105,8 @@ public DeserBean( .enumValue(Creator.class, "mode", SerdeConfig.SerCreatorMode.class) .orElse(null); delegating = creatorMode == SerdeConfig.SerCreatorMode.DELEGATING; - final Argument[] constructorArguments = introspection.getConstructorArguments(); + hasBuilder = introspection.hasBuilder(); + final Argument[] constructorArguments = hasBuilder ? introspection.builder().getBuildMethodArguments() : introspection.getConstructorArguments(); creatorSize = constructorArguments.length; PropertyNamingStrategy entityPropertyNamingStrategy = getPropertyNamingStrategy(introspection, decoderContext, null); @@ -205,52 +208,77 @@ public DeserBean( creatorPropertiesBuilder.register(propertyName, derProperty, true); } - final List> beanProperties = introspection.getBeanProperties() + if (hasBuilder) { + PropertiesBag.Builder readPropertiesBuilder = new PropertiesBag.Builder<>(introspection); + BeanIntrospection.Builder builder = introspection.builder(); + @NonNull Argument[] builderArguments = builder.getBuilderArguments(); + + for (int i = 0; i < builderArguments.length; i++) { + Argument builderArgument = (Argument) builderArguments[i]; + AnnotationMetadata annotationMetadata = builderArgument.getAnnotationMetadata(); + PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(annotationMetadata, decoderContext, entityPropertyNamingStrategy); + final String jsonProperty = resolveName(builderArgument, annotationMetadata, propertyNamingStrategy); + final DerProperty derProperty = new DerProperty<>( + conversionService, + introspection, + i, + jsonProperty, + builderArgument, + null, + null, + null + ); + readPropertiesBuilder.register(jsonProperty, derProperty, true); + } + readProperties = readPropertiesBuilder.build(); + } else { + + final List> beanProperties = introspection.getBeanProperties() .stream().filter(bp -> { final AnnotationMetadata annotationMetadata = bp.getAnnotationMetadata(); return !bp.isReadOnly() && - !annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.WRITE_ONLY).orElse(false) && - !annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED).orElse(false); + !annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.WRITE_ONLY).orElse(false) && + !annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED).orElse(false); }).toList(); - final Collection> beanMethods = introspection.getBeanMethods(); - final List> jsonSetters = new ArrayList<>(beanMethods.size()); - BeanMethod anySetter = null; - for (BeanMethod method : beanMethods) { - if (method.isAnnotationPresent(SerdeConfig.SerSetter.class)) { - jsonSetters.add(method); - } else if (method.isAnnotationPresent(SerdeConfig.SerAnySetter.class) && ArrayUtils.isNotEmpty(method.getArguments())) { - anySetter = method; + final Collection> beanMethods = introspection.getBeanMethods(); + final List> jsonSetters = new ArrayList<>(beanMethods.size()); + BeanMethod anySetter = null; + for (BeanMethod method : beanMethods) { + if (method.isAnnotationPresent(SerdeConfig.SerSetter.class)) { + jsonSetters.add(method); + } else if (method.isAnnotationPresent(SerdeConfig.SerAnySetter.class) && ArrayUtils.isNotEmpty(method.getArguments())) { + anySetter = method; + } } - } - if (anySetterValue == null) { - anySetterValue = (anySetter != null ? new AnySetter(anySetter) : null); - } + if (anySetterValue == null) { + anySetterValue = (anySetter != null ? new AnySetter(anySetter) : null); + } - if (CollectionUtils.isNotEmpty(beanProperties) || CollectionUtils.isNotEmpty(jsonSetters)) { - PropertiesBag.Builder readPropertiesBuilder = new PropertiesBag.Builder<>(introspection); - for (int i = 0; i < beanProperties.size(); i++) { - BeanProperty beanProperty = beanProperties.get(i); - PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(beanProperty.getAnnotationMetadata(), decoderContext, entityPropertyNamingStrategy); - final AnnotationMetadata annotationMetadata = beanProperty.getAnnotationMetadata(); - if (annotationMetadata.isAnnotationPresent(SerdeConfig.SerAnySetter.class)) { - anySetterValue = new AnySetter(beanProperty); - } else { - final boolean isUnwrapped = annotationMetadata.hasAnnotation(SerdeConfig.SerUnwrapped.class); - final Argument t = resolveArgument(beanProperty.asArgument()); - - if (isUnwrapped) { - if (unwrappedProperties == null) { - unwrappedProperties = new ArrayList<>(); - } - final DeserBean unwrapped = deserBeanRegistry.getDeserializableBean( + if (CollectionUtils.isNotEmpty(beanProperties) || CollectionUtils.isNotEmpty(jsonSetters)) { + PropertiesBag.Builder readPropertiesBuilder = new PropertiesBag.Builder<>(introspection); + for (int i = 0; i < beanProperties.size(); i++) { + BeanProperty beanProperty = beanProperties.get(i); + PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(beanProperty.getAnnotationMetadata(), decoderContext, entityPropertyNamingStrategy); + final AnnotationMetadata annotationMetadata = beanProperty.getAnnotationMetadata(); + if (annotationMetadata.isAnnotationPresent(SerdeConfig.SerAnySetter.class)) { + anySetterValue = new AnySetter(beanProperty); + } else { + final boolean isUnwrapped = annotationMetadata.hasAnnotation(SerdeConfig.SerUnwrapped.class); + final Argument t = resolveArgument(beanProperty.asArgument()); + + if (isUnwrapped) { + if (unwrappedProperties == null) { + unwrappedProperties = new ArrayList<>(); + } + final DeserBean unwrapped = deserBeanRegistry.getDeserializableBean( t, decoderContext - ); - final AnnotationMetadataHierarchy combinedMetadata = + ); + final AnnotationMetadataHierarchy combinedMetadata = new AnnotationMetadataHierarchy(annotationMetadata, - t.getAnnotationMetadata()); - unwrappedProperties.add(new DerProperty<>( + t.getAnnotationMetadata()); + unwrappedProperties.add(new DerProperty<>( conversionService, introspection, i, @@ -260,30 +288,30 @@ public DeserBean( beanProperty, null, unwrapped - )); - String prefix = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.PREFIX).orElse(""); - String suffix = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.SUFFIX).orElse(""); + )); + String prefix = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.PREFIX).orElse(""); + String suffix = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.SUFFIX).orElse(""); - PropertiesBag unwrappedProps = (PropertiesBag) unwrapped.readProperties; - if (unwrappedProps != null) { - for (Map.Entry> e : unwrappedProps.getProperties()) { - String resolved = prefix + e.getKey() + suffix; - readPropertiesBuilder.register(resolved, e.getValue(), false); + PropertiesBag unwrappedProps = (PropertiesBag) unwrapped.readProperties; + if (unwrappedProps != null) { + for (Map.Entry> e : unwrappedProps.getProperties()) { + String resolved = prefix + e.getKey() + suffix; + readPropertiesBuilder.register(resolved, e.getValue(), false); + } } - } - final PropertiesBag unwrappedCreatorParams = unwrapped.creatorParams; - if (unwrappedCreatorParams != null) { - for (Map.Entry> e : unwrappedCreatorParams.getProperties()) { - String resolved = prefix + e.getKey() + suffix; - //noinspection unchecked - creatorPropertiesBuilder.register(resolved, (DerProperty) e.getValue(), false); + final PropertiesBag unwrappedCreatorParams = unwrapped.creatorParams; + if (unwrappedCreatorParams != null) { + for (Map.Entry> e : unwrappedCreatorParams.getProperties()) { + String resolved = prefix + e.getKey() + suffix; + //noinspection unchecked + creatorPropertiesBuilder.register(resolved, (DerProperty) e.getValue(), false); + } } - } - } else { + } else { - final String jsonProperty = resolveName(beanProperty, annotationMetadata, propertyNamingStrategy); - final DerProperty derProperty = new DerProperty<>( + final String jsonProperty = resolveName(beanProperty, annotationMetadata, propertyNamingStrategy); + final DerProperty derProperty = new DerProperty<>( conversionService, introspection, i, @@ -292,15 +320,15 @@ public DeserBean( beanProperty, null, null - ); - readPropertiesBuilder.register(jsonProperty, derProperty, true); + ); + readPropertiesBuilder.register(jsonProperty, derProperty, true); + } } } - } - for (BeanMethod jsonSetter : jsonSetters) { - PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(jsonSetter.getAnnotationMetadata(), decoderContext, entityPropertyNamingStrategy); - final String property = resolveName( + for (BeanMethod jsonSetter : jsonSetters) { + PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(jsonSetter.getAnnotationMetadata(), decoderContext, entityPropertyNamingStrategy); + final String property = resolveName( new AnnotatedElement() { @Override public String getName() { @@ -314,9 +342,9 @@ public AnnotationMetadata getAnnotationMetadata() { }, jsonSetter.getAnnotationMetadata(), propertyNamingStrategy - ); - final Argument argument = resolveArgument((Argument) jsonSetter.getArguments()[0]); - final DerProperty derProperty = new DerProperty<>( + ); + final Argument argument = resolveArgument((Argument) jsonSetter.getArguments()[0]); + final DerProperty derProperty = new DerProperty<>( conversionService, introspection, 0, @@ -325,12 +353,13 @@ public AnnotationMetadata getAnnotationMetadata() { null, jsonSetter, null - ); - readPropertiesBuilder.register(property, derProperty, true); + ); + readPropertiesBuilder.register(property, derProperty, true); + } + readProperties = readPropertiesBuilder.build(); + } else { + readProperties = null; } - readProperties = readPropertiesBuilder.build(); - } else { - readProperties = null; } this.anySetter = anySetterValue; @@ -796,6 +825,33 @@ public void deserializeAndSetPropertyValue(Decoder objectDecoder, Deserializer.D } } + @Nullable + public void deserializeAndCallBuilder(Decoder objectDecoder, Deserializer.DecoderContext decoderContext, BeanIntrospection.Builder builder) throws IOException { + try { + P value = deserializer.deserializeNullable(objectDecoder, decoderContext, argument); + if (value == null && !nullable) { + if (!explicitlyRequired) { + value = defaultValue; + if (value == null) { + if (!mustSetField) { + return; + } + value = deserializer.getDefaultValue(decoderContext, argument); + } + } else { + throw new SerdeException("Unable to deserialize type [" + instrospection.getBeanType().getName() + "]. Required property [" + argument + + "] is not present in supplied data"); + } + } + builder.with(index, argument, value); + } catch (InvalidFormatException e) { + throw new InvalidPropertyFormatException(e, argument); + } catch (Exception e) { + throw new SerdeException("Error decoding property [" + argument + "] of type [" + instrospection.getBeanType() + "]: " + e.getMessage(), e); + } + } + + } private static AnnotationMetadata resolveArgumentMetadata(BeanIntrospection instrospection, Argument

argument, AnnotationMetadata annotationMetadata) { diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java index 80344fe36..f8a1eef49 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java @@ -71,6 +71,15 @@ public Deserializer createSpecific(DecoderContext context, Argument type1) -> decoder.decodeArbitrary(); } DeserBean deserBean = getDeserializableBean(type, context); + if (deserBean.hasBuilder) { + return new SimpleBuilderDeserializer( + deserBean.readProperties, + deserBean.introspection, + preInstantiateCallback, + ignoreUnknown, + strictNullable + ); + } if (deserBean.simpleBean) { return new SimpleObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback); } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleBuilderDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleBuilderDeserializer.java new file mode 100644 index 000000000..e5c0e85e8 --- /dev/null +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleBuilderDeserializer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.serde.support.deserializers; + +import java.io.IOException; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.Decoder; +import io.micronaut.serde.Deserializer; +import io.micronaut.serde.exceptions.SerdeException; + +final class SimpleBuilderDeserializer implements Deserializer { + private final PropertiesBag builderParameters; + private final BeanIntrospection introspection; + @Nullable + private final SerdeDeserializationPreInstantiateCallback preInstantiateCallback; + private final boolean ignoreUnknown; + private final boolean strictNullable; + + SimpleBuilderDeserializer( + PropertiesBag builderParameters, + BeanIntrospection introspection, + SerdeDeserializationPreInstantiateCallback preInstantiateCallback, boolean ignoreUnknown, boolean strictNullable) { + this.builderParameters = builderParameters; + this.introspection = introspection; + this.preInstantiateCallback = preInstantiateCallback; + this.ignoreUnknown = ignoreUnknown; + this.strictNullable = strictNullable; + } + + @Override + public Object deserialize(Decoder decoder, DecoderContext context, Argument type) throws IOException { + BeanIntrospection.Builder builder = introspection.builder(); + Decoder objectDecoder = decoder.decodeObject(type); + + if (builderParameters != null) { + PropertiesBag.Consumer propertiesConsumer = builderParameters.newConsumer(); + + boolean allConsumed = false; + while (!allConsumed) { + final String prop = objectDecoder.decodeKey(); + if (prop == null) { + break; + } + final DeserBean.DerProperty consumedProperty = propertiesConsumer.consume(prop); + if (consumedProperty != null) { + consumedProperty.deserializeAndCallBuilder(objectDecoder, context, builder); + allConsumed = propertiesConsumer.isAllConsumed(); + + } else if (ignoreUnknown) { + objectDecoder.skipValue(); + } else { + throw unknownProperty(type, prop); + } + } + } + + if (ignoreUnknown) { + objectDecoder.finishStructure(true); + } else { + String unknownProp = objectDecoder.decodeKey(); + if (unknownProp != null) { + throw unknownProperty(type, unknownProp); + } + objectDecoder.finishStructure(); + } + + return builder.build(); + } + + @Override + public Object deserializeNullable(@NonNull Decoder decoder, @NonNull DecoderContext context, @NonNull Argument type) throws IOException { + if (decoder.decodeNull()) { + return null; + } + return deserialize(decoder, context, type); + } + + private SerdeException unknownProperty(Argument beanType, String prop) { + return new SerdeException("Unknown property [" + prop + "] encountered during deserialization of type: " + beanType); + } +}