1

I'm working with an API that returns an empty string instead of null to indicate the existence of a property, i.e.:

{ 
  "id": 1,
  "child": { "foo": "bar" } // child exists
}

{ 
  "id": 1,
  "child": "" // child does not exist
}

This does not play nice with a serializable data class:

@Serializable
data class Parent(val id: Int, val child: Child?)

@Serializable
data class Child(val foo: String)

// Error when child is empty string:
kotlinx.serialization.json.internal.JsonDecodingException: Expected class
kotlinx.serialization.json.JsonObject (Kotlin reflection is not available) as
the serialized body of com.mypackage.Parent, but had class
kotlinx.serialization.json.JsonLiteral (Kotlin reflection is not available)

I "fixed" this issue by writing a custom serializer:

@Serializable(Parent.Companion::class)
data class Parent(val id: Int, val child: Child?) {
  companion object : KSerializer<Parent> {
    override val descriptor = PrimitiveSerialDescriptor(
      "Parent",
      PrimitiveKind.STRING
    )

    override fun deserialize(decoder: Decoder): Parent {
      val decoder = decoder as? JsonDecoder
        ?: throw SerializationException("Expected JSON decoder.")

      val jsonObject = decoder.decodeJsonElement() as? JsonObject
        ?: throw SerializationException("Expected JSON object at root.")

      if (jsonObject.containsKey("id").not()) throw SerializationException(
        message = "Field 'id' is missing."
      )

      val childElement = jsonObject.get("child")
      val child: Child?

      if (childElement == null || childElement is JsonPrimitive) {
        child = null
      } else {
        child = decoder.json.decodeFromJsonElement(childElement)
      }

      return Parent(id, child)
    }

    override fun serialize(encoder: Encoder, value: Parent) {
      Parent.serializer().serialize(encoder, value)
    }
  }
}

However, this is fairly tedious and potentially untenable for a data class with many properties. What I'd like to do is have the ability to opt-in individual properties to perform the check from the serialization code above, possibly with an annotation:

@Serializable
data class Parent(
  val id: Int,
  @NullOnSerializationFail val child: Child?
)

I'm new to Kotlin and haven't programmed in a Java environment for some time. Coming from Swift, something like this could be accomplished with a property wrapper, but from what I can gather, Kotlin/Java annotations don't work the same way.

Any ideas on how I could perform these kind of checks without writing per-class serialization code?

1
  • Try to use Jackson, you will avoid such problems. kotlinx.serialization is mostly for the multiplatform projects Commented Apr 2, 2024 at 16:24

1 Answer 1

0

After some digging, I found a solution that works fairly well and reduces boilerplate deserialization code. Step one is to create a custom abstract serializer class, like so:

abstract class NullIfNotObjectSerializer<T>(private val serializer: KSerializer<T>) : KSerializer<T> {
    override val descriptor = serializer.descriptor

    override fun serialize(encoder: Encoder, value: T) {
        require(encoder is JsonEncoder)
        encoder.json.encodeToJsonElement(serializer, value)
    }

    override fun deserialize(decoder: Decoder): T {
        require(decoder is JsonDecoder)
        val element = decoder.decodeJsonElement()

        return if (element is JsonObject) {
            decoder.json.decodeFromJsonElement(serializer, element)
        } else {
            decoder.json.decodeFromJsonElement(serializer, JsonNull)
        }
    }
}

NullIfNotObjectSerializer takes an external serializer as a parameter, which it uses as a proxy for its own KSerializer implementation. During deserialization, it checks to see if the json element trying to be decoded is an object. If so, it continues as normal, decoding the element using the proxy serializer. If not, it decodes a null json element.

To use this, declare an object with NullIfNotObjectSerializer as its super type, specifying T based on the type being decoded. Then use the @Serializable(with =) annotation on any property of said type that should use the serializer object. Given the parent/child example from my question above:

@Serializable
data class Parent(
  val id: Int,
  @Serializable(with = ChildSerializer::class) 
  val child: Child?
) {
  object ChildSerializer : NullIfNotObjectSerializer<Child?>(serializer<Child?>())
}

Note: I put ChildSerializer as an inner object of Parent for encapsulation purposes, but this isn't necessary.

So there it is. Certainly much better than having to write custom deserialization for every data class that might need this kind of protection.

Musings

I can't help but wonder if there's room for improvement. I'm curious if there's a way to skip the serializer object creation and go directly to something like this:

@Serializable
data class Parent(
  val id: Int,
  @Serializable(NullIfNotObjectSerializer::class)
  val child: Child?
) 

Or maybe even:

@Serializable
data class Parent(
  val id: Int,
  @NullIfNotObject val child: Child?
) 

For now I'm going to leave this question without an accepted answer. While I'm happy enough with my solution, I'm not sure it's the solution and I'm curious to see others' opinions.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.