Skip to content

Commit

Permalink
Support unwrapped subtype beans (#624)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov authored Oct 30, 2023
1 parent bf0573e commit d952f97
Show file tree
Hide file tree
Showing 14 changed files with 808 additions and 352 deletions.
48 changes: 48 additions & 0 deletions serde-api/src/main/java/io/micronaut/serde/ObjectSerializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2017-2021 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;

import io.micronaut.core.annotation.Indexed;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.type.Argument;

import java.io.IOException;

/**
* A variation of {@link Serializer} that is serializing an object and supports serializing its content into an existing object.
*
* @param <T> The object type to be serialized
* @author Denis Stepanov
* @since 2.3
*/
@Indexed(ObjectSerializer.class)
public interface ObjectSerializer<T> extends Serializer<T> {

/**
* Serializes the object values using the passed object encoder.
*
* @param encoder The object encoder to use
* @param context The encoder context, never {@code null}
* @param type Models the generic type of the value
* @param value The value, can be {@code null}
* @throws IOException If an error occurs during serialization
*/
void serializeInto(@NonNull Encoder encoder,
@NonNull EncoderContext context,
@NonNull Argument<? extends T> type,
@NonNull T value) throws IOException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,6 @@ class InnerFooId {
context.close()
}


void "test @JsonUnwrapped - levels 2"() {
given:
def ctx = buildContext("")
Expand Down Expand Up @@ -739,4 +738,232 @@ class InnerFooId {
cleanup:
ctx.close()
}

void "test @JsonUnwrapped - subtyping"() {
given:
def context = buildContext("""
package unwrapped;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
@Introspected(accessKind = Introspected.AccessKind.FIELD)
class Wrapper {
@JsonUnwrapped
public final SuperClass name;
Wrapper(@JsonUnwrapped SuperClass name) {
this.name = name;
}
}
@Serdeable
@Introspected(accessKind = Introspected.AccessKind.FIELD)
abstract class SuperClass {
public final String first;
SuperClass(String first) {
this.first = first;
}
}
@Serdeable
@Introspected(accessKind = Introspected.AccessKind.FIELD)
class SubClass extends SuperClass {
public final String last;
SubClass(String first, String last) {
super(first);
this.last = last;
}
}
""")
when:
def name = newInstance(context, 'unwrapped.SubClass', "Fred", "Flinstone")
def wrapper = newInstance(context, 'unwrapped.Wrapper', name)

def result = writeJson(jsonMapper, wrapper)

then:
result == '{"first":"Fred","last":"Flinstone"}'

cleanup:
context.close()
}

void 'test wrapped subtype with property info'() {
given:
def context = buildContext('test.Base', """
package test;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
@Introspected(accessKind = Introspected.AccessKind.FIELD)
class Wrapper {
public final String foo;
@JsonUnwrapped
public final Base base;
Wrapper(String foo, @JsonUnwrapped Base base) {
this.base = base;
this.foo = foo;
}
}
@Serdeable
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(
@JsonSubTypes.Type(value = Sub.class, name = "sub-class")
)
class Base {
private String string;
public Base(String string) {
this.string = string;
}
public String getString() {
return string;
}
}
@Serdeable
class Sub extends Base {
private Integer integer;
public Sub(String string, Integer integer) {
super(string);
this.integer = integer;
}
public Integer getInteger() {
return integer;
}
}
""")
when:
def base = newInstance(context, 'test.Sub', "a", 1)
def wrapper = newInstance(context, 'test.Wrapper', "bar", base)

def result = writeJson(jsonMapper, wrapper)

then:
result == '{"foo":"bar","type":"sub-class","string":"a","integer":1}'

when:
result = jsonMapper.readValue(result, argumentOf(context, "test.Wrapper"))

then:
result.foo == 'bar'
result.base.getClass().name == 'test.Sub'
result.base.string == 'a'
result.base.integer == 1

when:
result = jsonMapper.readValue('{"string":"a","integer":1,"type":"sub-class","foo":"bar"}', argumentOf(context, "test.Wrapper"))

then:
result.foo == 'bar'
result.base.getClass().name == 'test.Sub'
result.base.string == 'a'
result.base.integer == 1

when:
result = jsonMapper.readValue('{"foo":"bar", "type":"some-other-type","string":"a","integer":1}', argumentOf(context, "test.Wrapper"))

then:
result.getClass().name != 'test.Sub'

when:
result = jsonMapper.readValue('{"string":"a","integer":1,"foo":"bar","type":"Sub"}', argumentOf(context, "test.Wrapper"))

then:
result.getClass().name != 'test.Sub'
}

void 'test wrapped subtype with wrapper info'() {
given:
def context = buildContext('test.Base', """
package test;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
@Introspected(accessKind = Introspected.AccessKind.FIELD)
class Wrapper {
public final String foo;
@JsonUnwrapped
public final Base base;
Wrapper(String foo, @JsonUnwrapped Base base) {
this.base = base;
this.foo = foo;
}
}
@Serdeable
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
@JsonSubTypes(
@JsonSubTypes.Type(value = Sub.class, name = "subClass")
)
class Base {
private String string;
public Base(String string) {
this.string = string;
}
public String getString() {
return string;
}
}
@Serdeable
class Sub extends Base {
private Integer integer;
public Sub(String string, Integer integer) {
super(string);
this.integer = integer;
}
public Integer getInteger() {
return integer;
}
}
""")
when:
def result = jsonMapper.readValue('{"foo":"bar","subClass":{"string":"a","integer":1}}', argumentOf(context, "test.Wrapper"))

then:
result.foo == 'bar'
result.base.getClass().name == 'test.Sub'
result.base.string == 'a'
result.base.integer == 1

when:
result = jsonMapper.readValue('{"subClass":{"string":"a","integer":1}, "foo":"bar"}', argumentOf(context, "test.Wrapper"))

then:
result.foo == 'bar'
result.base.getClass().name == 'test.Sub'
result.base.string == 'a'
result.base.integer == 1

when:
def json = writeJson(jsonMapper, result)

then:
json == '{"foo":"bar","subClass":{"string":"a","integer":1}}'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import io.micronaut.inject.annotation.AnnotationMetadataHierarchy;
import io.micronaut.serde.Decoder;
import io.micronaut.serde.Deserializer;
import io.micronaut.serde.config.DeserializationConfiguration;
import io.micronaut.serde.config.annotation.SerdeConfig;
import io.micronaut.serde.config.naming.PropertyNamingStrategy;
import io.micronaut.serde.exceptions.InvalidFormatException;
Expand Down Expand Up @@ -103,7 +104,8 @@ final class DeserBean<T> {

// CHECKSTYLE:ON

public DeserBean(Argument<T> type,
public DeserBean(DeserializationConfiguration deserializationConfiguration,
Argument<T> type,
BeanIntrospection<T> introspection,
Deserializer.DecoderContext decoderContext,
DeserBeanRegistry deserBeanRegistry,
Expand All @@ -128,7 +130,7 @@ public DeserBean(Argument<T> type,
creatorSize = constructorArguments.length;
PropertyNamingStrategy entityPropertyNamingStrategy = getPropertyNamingStrategy(introspection, decoderContext, null);

this.ignoreUnknown = introspection.booleanValue(SerdeConfig.SerIgnored.class, "ignoreUnknown").orElse(true);
this.ignoreUnknown = introspection.booleanValue(SerdeConfig.SerIgnored.class, "ignoreUnknown").orElse(deserializationConfiguration.isIgnoreUnknown());
final PropertiesBag.Builder<T> creatorPropertiesBuilder = new PropertiesBag.Builder<>(introspection, constructorArguments.length);
List<DerProperty<T, ?>> creatorUnwrapped = null;
AnySetter<Object> anySetterValue = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@
@BootstrapContextCompatible
public class ObjectDeserializer implements CustomizableDeserializer<Object>, DeserBeanRegistry {
private final SerdeIntrospections introspections;
private final boolean ignoreUnknown;
private final boolean strictNullable;
private final DeserializationConfiguration deserializationConfiguration;
private final Map<BeanDefKey, Supplier<DeserBean<?>>> deserBeanMap = new ConcurrentHashMap<>(50);
@Nullable
private final SerdeDeserializationPreInstantiateCallback preInstantiateCallback;
Expand All @@ -58,8 +57,7 @@ public ObjectDeserializer(SerdeIntrospections introspections,
DeserializationConfiguration deserializationConfiguration,
@Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) {
this.introspections = introspections;
this.ignoreUnknown = deserializationConfiguration.isIgnoreUnknown();
this.strictNullable = deserializationConfiguration.isStrictNullable();
this.deserializationConfiguration = deserializationConfiguration;
this.preInstantiateCallback = preInstantiateCallback;
}

Expand Down Expand Up @@ -99,13 +97,13 @@ public Deserializer<Object> createSpecific(DecoderContext context, Argument<? su
private Deserializer<Object> findDeserializer(DeserBean<? super Object> deserBean, boolean isSubtype) {
Deserializer<Object> deserializer;
if (deserBean.simpleBean) {
deserializer = new SimpleObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback);
deserializer = new SimpleObjectDeserializer(deserializationConfiguration.isStrictNullable(), deserBean, preInstantiateCallback);
} else if (deserBean.recordLikeBean) {
deserializer = new SimpleRecordLikeObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback);
deserializer = new SimpleRecordLikeObjectDeserializer(deserializationConfiguration.isStrictNullable(), deserBean, preInstantiateCallback);
} else if (deserBean.delegating) {
deserializer = new DelegatingObjectDeserializer(strictNullable, deserBean, preInstantiateCallback);
deserializer = new DelegatingObjectDeserializer(deserializationConfiguration.isStrictNullable(), deserBean, preInstantiateCallback);
} else {
deserializer = new SpecificObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback);
deserializer = new SpecificObjectDeserializer(deserializationConfiguration.isStrictNullable(), deserBean, preInstantiateCallback);
}
if (!isSubtype && deserBean.wrapperProperty != null) {
deserializer = new WrappedObjectDeserializer(
Expand Down Expand Up @@ -148,7 +146,7 @@ private <T> DeserBean<T> createDeserBean(Argument<T> type,
DecoderContext decoderContext) {
try {
final BeanIntrospection<T> deserializableIntrospection = introspections.getDeserializableIntrospection(type);
return new DeserBean<>(type, deserializableIntrospection, decoderContext, this, namePrefix, nameSuffix);
return new DeserBean<>(deserializationConfiguration, type, deserializableIntrospection, decoderContext, this, namePrefix, nameSuffix);
} catch (SerdeException e) {
throw new IntrospectionException("Error creating deserializer for type [" + type + "]: " + e.getMessage(), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ final class SimpleObjectDeserializer implements Deserializer<Object>, UpdatingDe
@Nullable
private final SerdeDeserializationPreInstantiateCallback preInstantiateCallback;

SimpleObjectDeserializer(boolean ignoreUnknown, boolean strictNullable,
SimpleObjectDeserializer(boolean strictNullable,
DeserBean<? super Object> deserBean,
@Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) {
this.ignoreUnknown = ignoreUnknown && deserBean.ignoreUnknown;
this.ignoreUnknown = deserBean.ignoreUnknown;
this.strictNullable = strictNullable;
this.introspection = deserBean.introspection;
this.properties = deserBean.injectProperties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,14 @@ final class SimpleRecordLikeObjectDeserializer implements Deserializer<Object>,
@Nullable
private final SerdeDeserializationPreInstantiateCallback preInstantiateCallback;

SimpleRecordLikeObjectDeserializer(boolean ignoreUnknown,
boolean strictNullable,
SimpleRecordLikeObjectDeserializer(boolean strictNullable,
DeserBean<? super Object> deserBean,
@Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) {
this.introspection = deserBean.introspection;
this.constructorParameters = deserBean.creatorParams;
this.valuesSize = deserBean.creatorSize;
this.preInstantiateCallback = preInstantiateCallback;
this.ignoreUnknown = ignoreUnknown && deserBean.ignoreUnknown;
this.ignoreUnknown = deserBean.ignoreUnknown;
this.strictNullable = strictNullable;
}

Expand Down
Loading

0 comments on commit d952f97

Please sign in to comment.