Cannot create default custom Serializer/Deserializer for String
Closed this issue · 14 comments
Expected Behavior
Create custom Serde<String>
definition that is used for all serializations/deserializations. Add @Primary
to make it primary.
Actual Behaviour
In reality it's not used unless one marks the fields with explicit annotations as follows
@Serdeable
@MappedEntity
data class Address(
@field:Id
@GeneratedValue
var id: Long? = null,
@Serdeable.Serializable(using = StringSerde::class)
@Serdeable.Deserializable(using = StringSerde::class)
@field:MappedProperty("address_line_1")
var addressLine1: String?,
...
)
Steps To Reproduce
Create custom String serialization in kotlin
@Primary
@Singleton
class StringSerde : Serde<String> {
override fun serialize(
encoder: Encoder,
context: Serializer.EncoderContext,
type: Argument<out String>,
value: String
) {
encoder.encodeString(value)
}
override fun deserialize(
decoder: Decoder,
context: Deserializer.DecoderContext,
type: Argument<in String>
): String? {
if (decoder.decodeNull()) return null
val string = decoder.decodeString()
return string.ifBlank { null }
}
}
Start using it and see that it's not actually used by default
Environment Information
- kotlin
- micronaut serde 1.3.2
Example Application
No response
Version
3.7.3
not sure what the use case is
Use case is that when incoming string is empty ""
then we set it as null
. Eg:
{
"id": 124,
"address_line_1": ""
}
would result in Kotlin object that has addressLine1
as null
.
Our web developers aren't really keen on mapping empty strings to null on their side, so I thought we could fix it on backend side.
you can probably create a factory that replaces these beans:
Using @Replaces
https://docs.micronaut.io/latest/api/io/micronaut/context/annotation/Replaces.html#factory--
I might be stupid but I don't get how can you replace bean with generic qualifier
Tried this but got nothing
@Factory
class CustomDeserializers {
@Singleton
@Replaces(
value = Deserializer::class,
factory = io.micronaut.serde.support.deserializers.CoreDeserializers::class
)
fun stringDeserializer(): Deserializer<String> = object : Deserializer<String> {
override fun deserialize(
decoder: Decoder,
context: Deserializer.DecoderContext,
type: Argument<in String>
): String? {
if (decoder.decodeNull()) return null
return decoder.decodeString().ifBlank { null }
}
override fun allowNull() = true
}
}
This issue thread didn't give enough insight either. Maybe you could help us out here?
actually @Replaces
is not needed, the following works (using Java as unfamiliar with Kotlin):
import java.io.IOException;
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Order;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.serde.Encoder;
import io.micronaut.serde.Serializer;
import jakarta.inject.Singleton;
@Factory
public class CustomSerializers {
@Singleton
@Order(-100) // prioritize over default
Serializer<String> stringSerializer() {
return new Serializer<String>() {
@Override
public void serialize(Encoder encoder,
EncoderContext context,
Argument<? extends String> type, String value) throws IOException {
if (StringUtils.isEmpty(value)) {
encoder.encodeNull();;
} else {
encoder.encodeString(value);
}
}
@Override
public boolean isEmpty(EncoderContext context, String value) {
return value == null || value.length() == 0;
}
};
}
}
oops realised you were after the deserializer, anyway same strategy will work
Indeed, I was trying the @Order
annotation as well, but that did not give me anything
@Factory
class CustomDeserializers {
@Singleton
@Order(1000) // tried -100 as well
fun stringDeserializer(): Deserializer<String> = object : Deserializer<String> {
override fun deserialize(
decoder: Decoder,
context: Deserializer.DecoderContext,
type: Argument<in String>
): String? {
if (decoder.decodeNull()) return null
return decoder.decodeString().ifBlank { null }
}
override fun allowNull() = true
}
}
Then, blaming kotlin I tried to do it in Java, but still, nothing
@Factory
public class CustomDeserializer {
@Singleton
@Order(-100)
Deserializer<String> stringDeserializer() {
return new Deserializer<String>() {
@Override
public String deserialize(@NotNull Decoder decoder, @NotNull DecoderContext context,
@NotNull Argument<? super String> type) throws IOException {
if (decoder.decodeNull()) return null;
String str = decoder.decodeString();
if (str.isBlank()) return null;
return str;
}
@Override
public boolean allowNull() {
return true;
}
};
}
}
If everything seems to be correct, I'll do test repo next week for this.
Also, would be nice to have some hints in the documentation that core classes cannot be serialized using Serde<Class>
Created test repo for the issue, made the code in Java so it would be easier for you @graemerocher
https://github.com/GeitV/string-deserializer-not-working
Currently, following test fails as empty request string is treated as empty request string, not as null.
https://github.com/GeitV/string-deserializer-not-working/blob/604d0ffd0835e23b97b26bfd17f4e7715ee7ba42/src/test/kotlin/com/example/PingTest.kt#L31-L33
Maybe default String deserializer should be configurable?
There is already "jackson.trim-strings" in https://docs.micronaut.io/latest/guide/configurationreference.html
Maybe it is good idea to implement "micronaut.serde.deserialization.trim-strings" with same logic: "Whether strings should be trimmed when deserializing (defaults to false). If the resulting string is an empty string, then null will be applied instead."
I agree with Andrus. When you serialize, then the serializer already makes empty strings into nulls.
Would be great to have the same functionality when deserializing as well.
Is there any ETA for this, @graemerocher ? I just realized I also need this for dates that arrive as "". This throws an error when deserializing a date that is sent to Micronaut as empty string: "Text '' could not be parsed at index 0"
It would be good to have a configuration as well for trimming all incoming strings.
it is not as simple as making it configurable, it is better to replace the entire implementation if you have custom needs. Decoding/encoding strings is a highly performance sensitive code path, so currently the current implementation is the most optimal.
it is not as simple as making it configurable, it is better to replace the entire implementation if you have custom needs. Decoding/encoding strings is a highly performance sensitive code path, so currently the current implementation is the most optimal.
How would one go about implementing a trimming function then? The following code does not work:
@Factory
public class CustomSerializers {
@Singleton
@Order(-100) // prioritize over default
Serializer<String> stringSerializer() {
return new Serializer<String>() {
@Override
public void serialize(Encoder encoder,
EncoderContext context,
Argument<? extends String> type, String value) throws IOException {
if (StringUtils.isEmpty(value)) {
encoder.encodeNull();;
} else {
encoder.encodeString(value);
}
}
@Override
public boolean isEmpty(EncoderContext context, String value) {
return value == null || value.length() == 0;
}
};
}
}
you could make the logic part of the getter/setter instead