jackson-module-kotlin icon indicating copy to clipboard operation
jackson-module-kotlin copied to clipboard

Kotlin FeatureRequest: Serialize null string to empty string

Open MartinX3 opened this issue 4 years ago • 8 comments

Is your feature request related to a problem? Please describe. Please give us an option to serialize null strings to empty strings in kotlin.

Describe the solution you'd like If a kotlin data class has a null string, please convert it to "" in the json. (We use nullable strings over empty strings, because the null handling is easier, than the empty string handling.

Usage example Some option in the ObjectMapperCustomzier would be nice.

Additional context I tried

@Singleton
class JacksonRegisterCustomObjectMapperCustomizer : ObjectMapperCustomizer {
    override fun customize(mapper: ObjectMapper) {
        val sp = DefaultSerializerProvider.Impl()
        sp.setNullValueSerializer(NullSerializer.instance)

        val stringModule = SimpleModule()
        stringModule.addSerializer(String::class.javaObjectType, StringSerializer())
        stringModule.addSerializer(String::class.javaPrimitiveType, StringSerializer())
        stringModule.addSerializer(String::class.java, StringSerializer())

        mapper.registerModule(stringModule).setSerializerProvider(sp)
    }


    class StringSerializer : JsonSerializer<String?>() {
        override fun serialize(string: String?, jsonGenerator: JsonGenerator, serializerProvider: SerializerProvider) {
            when {
                string.isNullOrBlank() -> jsonGenerator.writeString("")
                else -> jsonGenerator.writeString(string)
            }
        }
    }
}

but it omits null variables.

I also tried mapper.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); without success.

MartinX3 avatar May 05 '21 14:05 MartinX3

Are you aware of existence of @JsonSetter functionality for null-handling (via annotations and/or per-type "config overrides")? They are briefly explained wrt Jackson 2.9 features:

https://cowtowncoder.medium.com/jackson-2-9-features-b2a19029e9ff

specifically, for Strings, you could:

// Prevent assigned of `null` for any `String` property!
mapper.configOverride(String.class)
   .setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.FAIL));

or, in your case, use Nulls.AS_EMPTY to get "" assigned.

cowtowncoder avatar May 05 '21 16:05 cowtowncoder

@cowtowncoder Thank you for your response. Yes, I tried that without success. Maybe because I use Kotlin.

MartinX3 avatar May 05 '21 18:05 MartinX3

Ahhhh. My mistake; this feature only works for deserialization, not serialization.

One request: could you change the title to mention "serialization" (not "deserialization") -- easier to avoid mistakes like mine.

cowtowncoder avatar May 05 '21 18:05 cowtowncoder

@cowtowncoder Argh, I'm sorry, I mixed it by myself as well. English is not my native tongue. xD

MartinX3 avatar May 05 '21 19:05 MartinX3

Np at all, thanks!

cowtowncoder avatar May 05 '21 19:05 cowtowncoder

@cowtowncoder Just to confirm, I'm guessing because you transferred this that customizing serialization of nulls is something that should happen in this module, not in jackson-databind?

dinomite avatar May 07 '21 20:05 dinomite

@dinomite it's bit of a tricky case: it might make sense on either side, depending. But at this point request itself was for Kotlin handling which is why I moved it -- does not mean it could not result in another issue on jackson-databind side. But at this point I think the problem is that I would not want a String-specific handling on databind at least; there should be something more widely applicable. But I don't really have a good suggestion or plan for more general translation on output... come to think of this I am not even sure I think it belongs in Jackson (wrt "Validation and Transformation are out of scope"). I am not sure it does not, either. :)

So. I think this particular request makes sense here, but likely work, if any, would need to think carefully about what to do, if anything, and where.

Not sure if this helps but that's my current thinking.

cowtowncoder avatar May 08 '21 21:05 cowtowncoder

Makes sense, thanks for confirming.

dinomite avatar May 11 '21 10:05 dinomite

I have created a simple sample that accomplishes this. Since it is feasible with existing functionality and has not received any votes, this issue is closed.

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.introspect.Annotated
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import org.junit.Test
import kotlin.test.assertEquals

object NullStrToEmptyStrSerializer : JsonSerializer<String?>() {
    override fun serialize(string: String?, jsonGenerator: JsonGenerator, serializerProvider: SerializerProvider) {
        when {
            string.isNullOrBlank() -> jsonGenerator.writeString("")
            else -> jsonGenerator.writeString(string)
        }
    }
}

object NullStrToEmptyStrAnnotationIntrospector : NopAnnotationIntrospector() {
    override fun findNullSerializer(am: Annotated): Any? = am.takeIf { it.type.rawClass == String::class.java }
        ?.let { NullStrToEmptyStrSerializer }
}

data class D(val s: String?, val i: Int?)

class NullSerTest {
    @Test
    fun test() {
        val mapper = jacksonObjectMapper().setAnnotationIntrospector(NullStrToEmptyStrAnnotationIntrospector)

        assertEquals("""{"s":"","i":null}""", mapper.writeValueAsString(D(null, null)))
        assertEquals("""{"s":"aaa","i":1}""", mapper.writeValueAsString(D("aaa", 1)))
    }
}

k163377 avatar Feb 19 '23 07:02 k163377