Skip to content

Instantly share code, notes, and snippets.

@bastman
Last active August 3, 2022 09:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bastman/a9de729a1392fcbb375e20ca1373c57e to your computer and use it in GitHub Desktop.
Save bastman/a9de729a1392fcbb375e20ca1373c57e to your computer and use it in GitHub Desktop.
jackson-module-kotlin: InstantRangeDeserializer - How to deserialize ClosedRange<Instant> - fix: Cannot construct instance of `kotlin.ranges.ClosedRange`
/*
InstantRangeDeserializer.kt
jackson-module-kotlin: InstantRangeDeserializer - How to deserialize ClosedRange<Instant>
Type definition error: [simple type, class kotlin.ranges.ClosedRange];
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `kotlin.ranges.ClosedRange` (no Creators, like default constructor, exist):
abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
see: https://github.com/ExpediaGroup/graphql-kotlin/issues/1220
see: https://github.com/FasterXML/jackson-module-kotlin/issues/38
WARNING: ClosedRange<Int>, since ClosedRange<T> is mapped to InstantRangeDeserializer -> deser will fail!
A "proper" implementation for ClosedRange<T> would probably need to be based on ContextualDeserializer
or ReferenceTypeDeserializer
Meanwhile, one can switch to a use a wrapper for a ClosedRange<T> ...
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
data class DelegatedClosedRange<T:Comparable<T>>(
@JsonIgnore val delegate:ClosedRange<T>
):ClosedRange<T> by delegate {
// @JsonCreator constructor(@JsonProperty("start") start:T, @JsonProperty("end") endInclusive:T) : this(start..endInclusive) {}
override fun toString(): String = delegate.toString()
fun toClosedRange():ClosedRange<T> = start..endInclusive
companion object {
@JsonCreator
@JvmStatic
fun <T:Comparable<T>> fromJson(@JsonProperty("start") start:T, @JsonProperty("end") endInclusive:T):DelegatedClosedRange<T> =
DelegatedClosedRange(start..endInclusive)
}
}
*/
import com.fasterxml.jackson.module.kotlin.readValue
import java.time.Duration
import java.time.Instant
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
object InstantRangeDeserializer: StdDeserializer<ClosedRange<Instant>>(null as Class<*>?) {
override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): ClosedRange<Instant> {
val node: ObjectNode = jp.codec.readTree<ObjectNode>(jp)
val startNode:JsonNode = node["start"]
val endNode:JsonNode = node["end"]
val startNodeValue:Instant = startNode.toInstant(nodeName = "start")
val endNodeValue:Instant = endNode.toInstant(nodeName = "end")
val out:ClosedRange<Instant> = startNodeValue..endNodeValue
return out
}
private fun JsonNode.toInstant(nodeName:String):Instant = when(this) {
is TextNode -> {
val textValue:String = this.textValue()
try { Instant.parse(textValue) } catch (all:Exception) {
error("Can't parse $nodeName of type ${this::class.simpleName} with value: $textValue as Instant")
}
} else -> error("Can't parse $nodeName of type ${this::class.simpleName} as Instant")
}
}
data class Foo(
val m1: ClosedRange<Instant>,
val m2: ClosedRange<Instant>?,
val l1: List<ClosedRange<Instant>>,
val l2:List<ClosedRange<Instant>>?
)
fun main() {
val JSON:ObjectMapper = Jackson.defaultMapper()
.apply {
registerModule(SimpleModule("InstantRangeModule").apply {
addDeserializer(ClosedRange::class.java, InstantRangeDeserializer)
})
}
val a = Foo(
m1 = (Instant.now()..Instant.now() + Duration.ofDays(10)),
m2 = (Instant.now()..Instant.now() + Duration.ofDays(20)),
l1 = listOf(
(Instant.now()..Instant.now() + Duration.ofDays(10)),
(Instant.now()..Instant.now() + Duration.ofDays(20)),
),
l2 = listOf(
(Instant.now()..Instant.now() + Duration.ofDays(10)),
(Instant.now()..Instant.now() + Duration.ofDays(20)),
)
)
val b = Foo(
m1 = (Instant.now()..Instant.now() + Duration.ofDays(10)),
m2 = null,
l1 = listOf(
(Instant.now()..Instant.now() + Duration.ofDays(10)),
(Instant.now()..Instant.now() + Duration.ofDays(20)),
),
l2 = null
)
listOf(a,b).forEach { source:Foo->
println("================")
println("source : $source")
val sourceJson:String = JSON.writeValueAsString(source)
println("source to json: $sourceJson")
val sink: Foo = JSON.readValue(sourceJson)
println("sink from json: $sink")
val sinkJson:String = JSON.writeValueAsString(sink)
println("sink to json : $sourceJson")
if(sink != source) {
println("source: $source")
println("sink : $sink")
error("assertion failed! sink!=source")
}
if(sinkJson != sourceJson) {
println("sourceJson: $source")
println("sinkJson : $sink")
error("assertion failed! sinkJson!=sourceJson")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment