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

NonNull enum property has null value

Open rafalbednarczuk opened this issue 4 years ago • 14 comments

Describe the bug Deserializing "null" string as non-null enum gives null value.

To Reproduce

enum class SocialMediaPlatform {INSTAGRAM, FACEBOOK, YOUTUBE, TWITCH}
fun main() {
    val json = "null"
    val enum  : SocialMediaPlatform = ObjectMapper().readValue(json)
    println(enum == null)
}

this snippet prints true

Expected behavior Snippet should throw exception

Versions Kotlin: 1.3.72 Jackson-module-kotlin: 2.11.2 Jackson-databind: 2.11.0

rafalbednarczuk avatar Sep 20 '20 11:09 rafalbednarczuk

This happens with any type- not just enums. IIRC, Objectmapper::readValue always returns a "platform type" which can always be null. This is definitely unfortunate and I don't know what the most correct and least-suprising answer would be, but I suspect the best answer would be a split API where half the methods on ObjectMapper return T? and half return T and fail on "null"

ragnese avatar Sep 22 '20 00:09 ragnese

If Kotlin types indicate nullability, this is something that Kotlin module has to provide support for: jackson-databind has no immediate visibility to (or often, concept of) aspects of Kotlin that extend beyond Java. So this would (need to) be a new feature

Implementation-wise there are two parts to the problem:

  1. How to introspect additional requirement for type
  2. How to change handling to fail on null

It might be possible to implement both by overriding method

public JsonSetter.Value findSetterInfo(Annotated ann) { }

in [Jackson]AnnotationIntrospector implementation/subclass that Kotlin provides, so if and when (1) is known, it can by defauilt return Nulls.Fail for JsonSetter.Value.value(). I think this would actually achieve what is asked here, although obviously testing would be needed as root value / property value handling differs a bit (there may be gaps for root values, in particular)

Same approach could work for other types too, of course, not just Enums. It should work nicely with POJOs; some work might be needed for deserializers of JDK default types.

cowtowncoder avatar Sep 22 '20 16:09 cowtowncoder

fun main() {
    println(isNullable<String?>())
    println(isNullable<String>())

}

inline fun <reified T> isNullable(): Boolean = null is T

This is how to know if type is nullalble or not. Null check for non-nullable types can be implemented in ObjectMapper.readValue() functions in Extensions.kt file. This will work, but I don't think that's the best solution.

rafalbednarczuk avatar Sep 23 '20 19:09 rafalbednarczuk

@rafalbednarczuk This is from source calls, but the challenge is how to introspect this for given type: starting point is likely just type-erased Class (or with TypeReference it'd be super-type information, java.lang.reflect.Type).

cowtowncoder avatar Sep 23 '20 19:09 cowtowncoder

@cowtowncoder Does this new feature require changes on jackson-core level or only jackson-module-kotlin level? TypeReference class is inside jackson-core. I think this feature is required only by Kotlin devs. Java devs are used to handling nullables by themselves.

rafalbednarczuk avatar Sep 23 '20 20:09 rafalbednarczuk

@rafalbednarczuk It really depends on exactly how it can be implemented, but ideally only in Kotlin module. Core components can not really use kotlin-core in any form (nor features beyond JDK baseline); kotlin module can.

Ability to fail on nulls (or replace with "default" value) exists already and is usable from Java with @JsonSetter(Nulls.FAIL) added on property, or using config overrides. This was added 3 years ago in Jackson 2.9:

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

but the question here is that of trying to automatically apply it for non-nullable Kotlin types, so that users would not have to -- for example -- add annotations all over the place, or have to enumerate all enumerations to use config overrides.

cowtowncoder avatar Sep 23 '20 22:09 cowtowncoder

@cowtowncoder What do you think of modifying readValue functions? Here is working example snippet. I can make pull request for all functions.

fun main() {
    val json = "null"
    val mapper = JsonMapper.builder().build()

    //not throwing error
    val nullableInt: Int? = mapper.readValue(json)
    //throws error
    val nonNullableInt: Int = mapper.readValue(json)
}

inline fun <reified T> ObjectMapper.readValue(content: String): T = readValue(content, jacksonTypeRef<T>()).throwIfNullableTypeIsNull()

inline fun <reified T> T.throwIfNullableTypeIsNull(): T {
    if (null !is T && this == null) {
        throw Exception()
    }
    return this
}

rafalbednarczuk avatar Sep 24 '20 05:09 rafalbednarczuk

@rafalbednarczuk Main problem is just non-scalability (and fragility of sub-classing in general) of this approach: ObjectMapper has many methods, but there is also ObjectReader (which can not really be easily sub-classed by modules), and some formats extend ObjectMapper (like XmlMapper, CsvMapper for 2.x, but in 3.0 all format types will do this). That is why I would prefer finding other mechanism.

cowtowncoder avatar Sep 24 '20 21:09 cowtowncoder

@cowtowncoder Do you have any other solution? Do you plan to add this feature or should I compile my own fork?

rafalbednarczuk avatar Sep 24 '20 21:09 rafalbednarczuk

I have no plans to work on this myself, I am offering information that helps others to possibly implement it (and outline some challenges). So if you want to work on this that would be great of course!

cowtowncoder avatar Sep 24 '20 21:09 cowtowncoder

Let me know where to put the code and I will work on it.

rafalbednarczuk avatar Sep 24 '20 21:09 rafalbednarczuk

Typically you would fork whatever you want to contribute fix/change for.

cowtowncoder avatar Sep 25 '20 00:09 cowtowncoder

Opened PR 405 against 2.12 with @rafalbednarczuk's fix.

dinomite avatar Jan 01 '21 19:01 dinomite

Similar issue with Int:

val mapper = jacksonObjectMapper()

data class Car(val doors: Int)

val car: Car = mapper.readValue("{\"doors\":null }")
println(car)

Ideally this should ideally throw a MissingKotlinParameterException.
Instead it prints: Car(doors=0)

davidmoshal avatar Jan 09 '21 23:01 davidmoshal

This issue is closed as a duplicate of https://github.com/FasterXML/jackson-module-kotlin/issues/399.

k163377 avatar Jul 08 '23 10:07 k163377