Last active
August 3, 2022 09:27
-
-
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`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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