-
Notifications
You must be signed in to change notification settings - Fork 620
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow ListSerializer
to parse partial good results and ignore bad ones
#1205
Comments
ListSerializer
parse partial good results and ignore bad onesListSerializer
to parse partial good results and ignore bad ones
This is a problem with the format, not the |
To add some context and use case: I am using inline classes in the spirit of parse, don't validate. In particular I am doing something like: @Serializable
data class Cooperative(
val name: CooperativeName,
val city: CityName,
val country: CountryCode,
val latitude: Latitude,
val longitude: Longitude
) {
companion object {
fun setFromJSONString(string: String): Set<Cooperative> =
lenientJson
.decodeFromString<Set<Cooperative>>(string)
fun toJSONString(cooperatives: Set<Cooperative>) = lenientJson.encodeToString(cooperatives)
}
} where every type has specific init requirements, examples: class KUppercaseSerializer : KSerializer<String> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UppercaseString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String) = encoder.encodeString(value.uppercase())
override fun deserialize(decoder: Decoder): String = decoder.decodeString().uppercase()
}
@JvmInline
@Serializable
value class CooperativeName(val name: String) {
init {
require(name.isNotBlank())
require(name.trim() == name)
}
}
@JvmInline
@Serializable
value class CountryCode(
@Serializable(with = KUppercaseSerializer::class)
val code: String
) {
init {
require(code.length == 2)
require(code.all { it.isLetter() && it.isUpperCase() })
}
}
@JvmInline
@Serializable
value class Latitude(
@Serializable(with = KBigDecimalStringSerializer::class) val latitude: BigDecimal
) {
init {
require(latitude >= BigDecimal(-90))
require(latitude <= BigDecimal(90))
}
} Currently, I have to do some boilerplate code around this very strict parsers. Something like: @Serializable
private data class LaxCooperative(
val name: String,
val city: String,
val country: String,
val latitude: String? = null,
val longitude: String? = null
)
private fun validate(cooperative: LaxCooperative): Cooperative? =
try {
if (cooperative.mail !== null && cooperative.latitude !== null && cooperative.longitude !== null)
Cooperative(
CooperativeName(cooperative.name),
CityName(cooperative.city),
CountryCode(cooperative.country),
Latitude(BigDecimal(cooperative.latitude)),
Longitude(BigDecimal(cooperative.longitude)),
) else null
} catch (t: Throwable) { null } and change fun setFromJSONString(string: String): Set<Cooperative> =
lenientJson
.decodeFromString<Set<LaxCooperative>>(string)
.mapNotNull { validate(it) }
.toSet() This way I do a "lax" parse of my data class and then attempt to build the strict version. |
I made my own implementation Easy to apply it to list: @Serializable
private data class Data(val name: String, val value: Int)
val jsonString ="""[{"name":"Name1","value":11},{"name":"Name2","value":22}]"""
val serializer = kindListSerializer(Data.serializer())
Json.decodeFromString(serializer, jsonString) But what to do if list is property of serializable class? @Serializable
private data class DataContainer(
val name: String,
val data: List<Data>, // TODO ???
) |
It looks like the error is imprecise. If your |
@sandwwraith thank you! It works |
@sandwwraith but not always... |
A serializer for a type with generic parameter will need a constructor that takes a serializer for that generic parameter. In this case you want to use |
The solution I chose uses a wrapped value which becomes null if it cannot be decoded: /**
* Serializable class which wraps a value which is optionally decoded (in case the client does not have support for
* the corresponding value implementation).
*/
@Serializable(with = WrappedSerializer::class)
data class Wrapped<T : Any>(val value: T?) {
fun unwrapped(default: T): T {
return value ?: default
}
override fun toString(): String {
return value?.toString() ?: "null"
}
}
/**
* Serializer for [Wrapped] values.
*/
class WrappedSerializer<T : Any>(private val valueSerializer: KSerializer<T?>) : KSerializer<Wrapped<T>> {
override val descriptor: SerialDescriptor = valueSerializer.descriptor
private val objectSerializer = JsonObject.serializer()
override fun serialize(encoder: Encoder, value: Wrapped<T>) {
valueSerializer.serialize(encoder, value.value)
}
override fun deserialize(decoder: Decoder): Wrapped<T> {
val decoderProxy = DecoderProxy(decoder)
return try {
Wrapped(valueSerializer.deserialize(decoderProxy))
} catch (ex: Exception) {
println("Failed deserializing of ${valueSerializer.descriptor}: ${ex.message}")
// Consume the rest of the input if we are inside a structure
decoderProxy.compositeDecoder?.let {
decoderProxy.decodeSerializableValue(objectSerializer)
it.endStructure(valueSerializer.descriptor)
}
Wrapped(null)
}
}
}
private class DecoderProxy(private val decoder: Decoder) : Decoder by decoder {
var compositeDecoder: CompositeDecoder? = null
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
val compositeDecoder = decoder.beginStructure(descriptor)
this.compositeDecoder = compositeDecoder
return compositeDecoder
}
} with this class you can make Safe collections, for example a safe list which behaves just like a list using delegation: @Suppress("DataClassPrivateConstructor")
@Serializable(with = SafeList.Serializer::class)
data class SafeList<E : Any> private constructor(
private val _values: List<Wrapped<E>>
) : List<E> by _values.unwrappedList() {
constructor(values: Iterable<E>) : this(values.wrappedList())
constructor() : this(emptyList())
class Serializer<E: Any>(valueSerializer: KSerializer<E?>) : KSerializer<SafeList<E>> {
private val _serializer = ListSerializer(WrappedSerializer(valueSerializer))
override val descriptor: SerialDescriptor = _serializer.descriptor
override fun deserialize(decoder: Decoder): SafeList<E> {
return SafeList(_values = _serializer.deserialize(decoder))
}
override fun serialize(encoder: Encoder, value: SafeList<E>) {
_serializer.serialize(encoder, value._values)
}
}
override fun toString(): String {
return "[" + _values.unwrappedList().joinToString(", ") + "]"
}
companion object {
@JvmStatic
fun <E: Any>of(vararg elements: E) : SafeList<E> {
return safeListOf(*elements)
}
}
}
fun <E: Any>safeListOf(vararg elements: E) : SafeList<E> {
return SafeList(elements.asIterable())
}
/**
* Convenience function to wrap a list of [Any] values
*/
fun <T : Any> Iterable<T>.wrappedList(): List<Wrapped<T>> {
return this.map { Wrapped(it) }
}
/**
* Convenience function to unwrap a collection of [Wrapped] values to a list
*/
fun <T : Any> Iterable<Wrapped<T>>.unwrappedList(): List<T> {
return this.mapNotNull { it.value }
} Now instead of an ordinary List you would use a SafeList in your data objects: @Serializable
data class Foo(val bars: SafeList<Bar>)
@Serializable
data class Bar(val name: String) |
Has this still not landed yet? |
What is your use-case and why do you need this feature?
I'm receiving a list of items from the server. Some of the items might miss some required fields or be otherwise malformed.
In this case, I would like to ignore the items that failed to parse but still parse the ones that I can.
Unfortunately, it looks like
ListSerializer
is failing as soon as it encounters a single list item which it fails to parse.Describe the solution you'd like
Some sort of
ignoreFailedListItems
orallowPartialListResults
configuration.*This probably applies to all collections.
The text was updated successfully, but these errors were encountered: