Skip to content

Commit

Permalink
Remove use of AliasFor and use mappers for Serde annotations (#1012)
Browse files Browse the repository at this point in the history
using @AliasFor adds annotations as stereotypes which causes problems right now when you have multiple annotations as the values from the stereotypes are not resolved. This resolves that by using mappers instead

- **Reproduce the bug with `@Serdeable.` and `@JsonProperty`**
- **remove the use of @AliasFor and use mappers instead**


---------

Co-authored-by: Denis Stepanov <denis.s.stepanov@oracle.com>
  • Loading branch information
graemerocher and dstepanov authored Jan 9, 2025
1 parent b4d6c7c commit 88d8b56
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package io.micronaut.serde.annotation;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.serde.Deserializer;
import io.micronaut.serde.Serializer;
Expand Down Expand Up @@ -48,13 +47,11 @@
/**
* @return Whether build time validation should fail compilation on definition errors.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.VALIDATE)
boolean validate() default true;

/**
* @return Naming strategy to use for both serialization and deserialization.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.NAMING)
Class<? extends PropertyNamingStrategy> naming() default IdentityStrategy.class;

/**
Expand All @@ -67,26 +64,22 @@
/**
* @return The {@link io.micronaut.serde.Serializer} to use.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.SERIALIZER_CLASS)
Class<? extends Serializer> using() default Serializer.class;

/**
* @return Whether build time validation should fail compilation on definition errors.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.VALIDATE)
boolean validate() default true;

/**
* Use the given class to serialize this type.
* @return A type that is a subclass of the annotated type.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.SERIALIZE_AS)
Class<?> as() default void.class;

/**
* @return Naming strategy to use.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.NAMING)
Class<? extends PropertyNamingStrategy> naming() default IdentityStrategy.class;
}

Expand All @@ -100,26 +93,22 @@
/**
* @return The deserializer.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.DESERIALIZER_CLASS)
Class<? extends Deserializer> using() default Deserializer.class;

/**
* @return Whether build time validation should fail compilation on definition errors.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.VALIDATE)
boolean validate() default true;

/**
* Use the given class to deserialize this type.
* @return A type that is a subclass of the annotated type.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.DESERIALIZE_AS)
Class<?> as() default void.class;

/**
* @return Naming strategy to use.
*/
@AliasFor(annotation = SerdeConfig.class, member = SerdeConfig.NAMING)
Class<? extends PropertyNamingStrategy> naming() default IdentityStrategy.class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ package custom;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.context.annotation.Bean;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.type.Argument;
import io.micronaut.serde.annotation.Serdeable;
Expand All @@ -697,7 +698,66 @@ record CustomValue(
}
}
@Singleton
@Bean(typed = CustomSerde.class)
class CustomSerde implements Deserializer<Integer> {
@Override
public Integer getDefaultValue(DecoderContext ignoredContext, Argument<? super Integer> ignoredType) {
return -2;
}
@Override
public Integer deserialize(Decoder decoder, DecoderContext context, Argument<? super Integer> type) throws IOException {
return decoder.decodeInt();
}
@Override
public Integer deserializeNullable(Decoder decoder, DecoderContext context, Argument<? super Integer> type) throws IOException {
if (decoder.decodeNull()) {
return -1;
}
return decoder.decodeInt();
}
}
''')

expect:
jsonMapper.readValue('{"value":1}', typeUnderTest).value() == 1
jsonMapper.readValue('{"value":null}', typeUnderTest).value() == -1
jsonMapper.readValue('{}', typeUnderTest).value() == -2

cleanup:
context.close()
}
void "test custom deserializer 2"() {

given:
def context = buildContext('custom.CustomValue','''
package custom;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.context.annotation.Bean;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.type.Argument;
import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.serde.annotation.SerdeImport;
import io.micronaut.serde.Decoder;
import io.micronaut.serde.Deserializer;
import jakarta.inject.Singleton;
import java.io.IOException;
@Serdeable
record CustomValue(
@Serdeable.Deserializable(using = CustomSerde.class)
@NonNull
Integer value
) {
@JsonCreator
public CustomValue {
}
}
@Bean(typed = CustomSerde.class)
class CustomSerde implements Deserializer<Integer> {
@Override
public Integer getDefaultValue(DecoderContext ignoredContext, Argument<? super Integer> ignoredType) {
Expand Down Expand Up @@ -727,4 +787,99 @@ class CustomSerde implements Deserializer<Integer> {
cleanup:
context.close()
}

void "test custom serializer"() {
given:
def context = buildContext('custom.CustomValue','''
package custom;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.context.annotation.Bean;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.type.Argument;
import io.micronaut.serde.Encoder;
import io.micronaut.serde.Serializer;
import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.serde.annotation.SerdeImport;
import io.micronaut.serde.Decoder;
import jakarta.inject.Singleton;
import java.io.IOException;
@Serdeable
record CustomValue(
@Serdeable.Serializable(using = CustomSerde.class)
@JsonProperty("value")
@NonNull
Integer value
) {
@JsonCreator
public CustomValue {
}
}
@Bean(typed = CustomSerde.class)
class CustomSerde implements Serializer<Integer> {
@Override
public void serialize(Encoder encoder, EncoderContext context, Argument<? extends Integer> type, Integer value) throws IOException {
encoder.encodeInt(123);
}
}
''')

expect:
jsonMapper.writeValueAsString(typeUnderTest.type.newInstance(1)) == """{"value":123}"""
jsonMapper.writeValueAsString(typeUnderTest.type.newInstance(2)) == """{"value":123}"""
jsonMapper.writeValueAsString(typeUnderTest.type.newInstance(3)) == """{"value":123}"""

cleanup:
context.close()
}

void "test custom serializer 2"() {
given:
def context = buildContext('custom.CustomValue','''
package custom;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.context.annotation.Bean;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.type.Argument;
import io.micronaut.serde.Encoder;
import io.micronaut.serde.Serializer;
import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.serde.annotation.SerdeImport;
import io.micronaut.serde.Decoder;
import jakarta.inject.Singleton;
import java.io.IOException;
@Serdeable
record CustomValue(
@Serdeable.Serializable(using = CustomSerde.class)
@NonNull
Integer value
) {
@JsonCreator
public CustomValue {
}
}
@Bean(typed = CustomSerde.class)
class CustomSerde implements Serializer<Integer> {
@Override
public void serialize(Encoder encoder, EncoderContext context, Argument<? extends Integer> type, Integer value) throws IOException {
encoder.encodeInt(123);
}
}
''')

expect:
jsonMapper.writeValueAsString(typeUnderTest.type.newInstance(1)) == """{"value":123}"""
jsonMapper.writeValueAsString(typeUnderTest.type.newInstance(2)) == """{"value":123}"""
jsonMapper.writeValueAsString(typeUnderTest.type.newInstance(3)) == """{"value":123}"""

cleanup:
context.close()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2017-2025 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.processor.serde;

import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.AnnotationValueBuilder;
import io.micronaut.inject.annotation.TypedAnnotationMapper;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.serde.config.annotation.SerdeConfig;
import java.util.List;

public final class DeserializableMapper
implements TypedAnnotationMapper<Serdeable.Deserializable> {
@Override
public Class<Serdeable.Deserializable> annotationType() {
return Serdeable.Deserializable.class;
}

@Override
public List<AnnotationValue<?>> map(AnnotationValue<Serdeable.Deserializable> annotation, VisitorContext visitorContext) {
AnnotationValueBuilder<SerdeConfig> builder = AnnotationValue.builder(SerdeConfig.class);
annotation.annotationClassValue("as").ifPresent(as ->
builder
.member(SerdeConfig.DESERIALIZE_AS, as)
);
annotation.annotationClassValue("using").ifPresent(using ->
builder
.member(SerdeConfig.DESERIALIZER_CLASS, using)
);
annotation.booleanValue("validate").ifPresent(validation ->
builder
.member(SerdeConfig.VALIDATE, validation)
);
annotation.annotationClassValue("naming").ifPresent(naming ->
builder
.member(SerdeConfig.NAMING, naming)
);

return List.of(builder.build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2017-2025 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.processor.serde;

import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.AnnotationValueBuilder;
import io.micronaut.inject.annotation.TypedAnnotationMapper;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.serde.config.annotation.SerdeConfig;
import java.util.List;

public final class SerdeableMapper
implements TypedAnnotationMapper<Serdeable> {
@Override
public Class<Serdeable> annotationType() {
return Serdeable.class;
}

@Override
public List<AnnotationValue<?>> map(AnnotationValue<Serdeable> annotation, VisitorContext visitorContext) {

AnnotationValueBuilder<SerdeConfig> builder = AnnotationValue.builder(SerdeConfig.class);
annotation.booleanValue("validate").ifPresent(validation ->
builder
.member(SerdeConfig.VALIDATE, validation)
);
annotation.annotationClassValue("naming").ifPresent(naming ->
builder
.member(SerdeConfig.NAMING, naming)
);

AnnotationValue<SerdeConfig> result = builder.build();
if (!result.getValues().isEmpty()) {
return List.of(result);
} else {
return List.of();
}
}
}
Loading

0 comments on commit 88d8b56

Please sign in to comment.