I've got the following code: ```@Serializable dat...
# serialization
s
I've got the following code:
Copy code
@Serializable
data class ChangeOrderInput(
    val oldProposal: OldProposal,
    val newProposals: List<NewProposal>
)

@Serializable
data class OldProposal(
    val product: Product
)

@Serializable
data class NewProposal(
    val id: String,
    val product: Product,
    val changeOrderType: ChangeOrderType? = null
)

@Serializable(with = ProductSerializer::class)
sealed class Product {
    abstract val type: ProductType
    abstract val bundle: ProductBundle
}

object ProductSerializer : JsonContentPolymorphicSerializer<Product>(Product::class) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Product> {
        val typeString = element.jsonObject["type"]?.jsonPrimitive?.content ?: error("`type` wasn't provided for `product`")
        return when (ProductType.valueOf(typeString)) {
            ProductType.Cash -> CashProduct.serializer()
            <http://ProductType.Loan|ProductType.Loan> -> LoanProduct.serializer()
        }
    }
}

@Serializable
data class CashProduct(
    override val type: ProductType = ProductType.Cash,
    override val bundle: ProductBundle,
    val totalPrice: BigDecimal
) : Product()

@Serializable
data class LoanProduct(
    override val type: ProductType = <http://ProductType.Loan|ProductType.Loan>,
    override val bundle: ProductBundle,
    val vendor: String,
    val term: BigDecimal,
    val apr: BigDecimal,
    val totalPrice: BigDecimal
) : Product()

@Serializable
data class ExampleResponse(
    val version: String = "",
    val outputObjects: List<NewProposal>? = listOf()
)
When I serialize a ChangeOrderInput object:
Copy code
val inputJsonString = input.reader().use { it.readText() }
val request = Serialization.json.decodeFromString(ChangeOrderInput.serializer(), inputJsonString)
val response = runBlocking {
    changeOrderWrapper.handleRequest(request)
}
val outputJsonString = Serialization.json.encodeToString(response)
output.writer().use { it.write(outputJsonString) }
it serializes both the oldProposal and newProposal correctly (see request object in the image). But when I try to deserialize the
ExampleResponse
object, it deserializes everything except the
NewProposal#type
field. I just now thought that maybe this was due to the classDiscriminator property on the Serializer, but that is not it. I tried changing it to
#class
and it still has the same result. Anyone know what is going on here?
1
🧵 1
just to have all the relevant code, here's my Serialization object
Copy code
object Serialization {
    val json: Json by lazy {
        Json {
            ignoreUnknownKeys = true
            isLenient = true
            classDiscriminator = "#class"
        }
    }
}
e
encodeDefaults = false
by default
s
hm. even with that it still doesn't set the
type
field.
e
how about using the default polymorphic serializer?
Copy code
@Serializable
sealed class Product
@Serializable @SerialName("Cash")
class CashProduct : Product()
@Serializable @SerialName("Loan")
class LoanProduct : Product()
s
I haven't seen that example. I was working off this page, maybe I stopped reading too soon. https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md
so what is
@SerialName(..)
doing there?
s
Ah, I did read that, but I didn't understand it.
so then I wouldn't need the
JsonContentPolymorphicSerializer
at all?
e
as long as everything can be determined via the serial name, no you don't need JsonContentPolymorphicSerializer
nor the type property in the constructor (could still write
override val type get() = ProductType.Cash
etc. in the body if you find it useful)
s
ah yeah so with the default names like that I get this really weird error.
Copy code
Caused by: kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for missing class discriminator ('null')
JSON input: {"type":"Cash","bundle":"Base","totalPrice":"1.23"}
where it looks like it's still trying to deserialize nested objects with the
Product
serializer.
e
did you remove your customization to
#class
?
s
lol no. ok, let me remove that haha
hm...
Copy code
Caused by: java.lang.IllegalStateException: Sealed class 'Loan' cannot be serialized as base class 'com.sunrun.changeorder.resources.Product' because it has property name that conflicts with JSON class discriminator 'type'. You can either change class discriminator in JsonConfiguration, rename property with @SerialName annotation or fall back to array polymorphism
	at kotlinx.serialization.json.internal.PolymorphicKt.validateIfSealed(Polymorphic.kt:46)
	at kotlinx.serialization.json.internal.PolymorphicKt.findActualSerializer(Polymorphic.kt:30)
	at kotlinx.serialization.json.internal.PolymorphicKt.access$findActualSerializer(Polymorphic.kt:1)
	at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeSerializableValue(StreamingJsonEncoder.kt:273)
	at kotlinx.serialization.encoding.AbstractEncoder.encodeSerializableElement(AbstractEncoder.kt:80)
	at com.sunrun.changeorder.resources.NewProposal.write$Self(Rest.kt:27)
	at com.sunrun.changeorder.resources.NewProposal$$serializer.serialize(Rest.kt:27)
	at com.sunrun.changeorder.resources.NewProposal$$serializer.serialize(Rest.kt:27)
e
remove
type
from the constructors, e.g.
Copy code
data class CashProduct(/* no type */) : Product() {
    override val type: ProductType // or just remove this property altogether, if you don't need it
        get() = ProductType.Cash
}
as I mentioned earlier
s
hm. I do need the type. And I do need to be able to pass it in in a json payload. So kotlinx will see it in the payload, use it to find
CashProduct
, then create cash product without the type in the constructor, then I can have that property to still be able to get the given type?
e
the way I wrote it right there, yes
alternately,
data class CashProduct(@Transient val type: ProductType) : Product()
but IMO it's just confusing to allow it to be overridden from code when it won't be serialized
e.g.
CashProduct(type = <http://ProductType.LOAN|ProductType.LOAN>)
-> serialize -> deserialize -> doesn't make sense, regardless of which approach you used
👍🏽 1
s
🎉
yay! thank you
and yes I agree on that last comment. I'm so used to making data classes with all the values in the constructor that I continue to do so even when I don't need it.
probably a really bad habit.
question though. I was looking at the docs for the polymorphic deserializer and it mentions a
@Polymorphic
annotation. What is that? Why do I not need it? https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization/-polymorphic-serializer/index.html
e
well, data classes with mutable vars in the body is not great, but vals can only be set via what's passed in the constructor
in that example, notice that
BaseRequest
is a not a
sealed class
s
ah! I didn't see that.
gotcha.
e
instead of relying on a generated polymorphic serializer for it, the subtypes must be registered in a module
s
perfect. exactly the answer I needed. thank you so much.
one last question. why does
Copy code
override val type: ProductType 
        get() = <http://ProductType.Loan|ProductType.Loan>
work but
Copy code
override val type: ProductType = <http://ProductType.Loan|ProductType.Loan>
results in
Copy code
org.jboss.resteasy.spi.UnhandledException: java.lang.IllegalStateException: Sealed class 'Loan' cannot be
		serialized as base class 'com.sunrun.changeorder.resources.Product' because it has property name that conflicts
		with JSON class discriminator 'type'. You can either change class discriminator in JsonConfiguration, rename
		property with @SerialName annotation or fall back to array polymorphism
? Functionally they are the same are they not? Or is it because the latter results in setting the property on instantiation, while the former is just a getter, not set on instantiation?
e
they are not quite the same: the first has a computed getter only (thus is implicitly treated like
@Transient
, not serialized), while the latter has both a backing field and a getter
the latter has a pretty useless backing field because it's always the same constant value in all instances, but it's still there
s
I see. I thought the backing field was always generated, even with only a `get()`er.
e
nope, it's only generated if it's accessed
s
thanks for the great explanations. I'm loving kotlinx-serialization so far. Much better than jackson.
m
Hi @snowe did you manage to find a fix for this error:
Sealed class cannot be serialized as base class because it has property name that conflicts with JSON class discriminator 'type'. You can either change class discriminator in JsonConfiguration, rename property with @SerialName annotation or fall back to array polymorphism