JacksonHelper.kt
package com.hexagonkt.serialization.jackson
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_SINGLE_QUOTES
import com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES
import com.fasterxml.jackson.core.JsonToken.START_OBJECT
import com.fasterxml.jackson.core.json.JsonReadFeature.*
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
import com.fasterxml.jackson.databind.MapperFeature.SORT_PROPERTIES_ALPHABETICALLY
import com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.*
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import java.net.InetAddress
import java.nio.ByteBuffer
import java.util.Base64
object JacksonHelper {
fun createObjectMapper(mapperFactory: JsonFactory): JsonMapper =
JsonMapper
.builder(mapperFactory)
.configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(FAIL_ON_EMPTY_BEANS, false)
.configure(ALLOW_UNQUOTED_FIELD_NAMES, true)
.configure(ALLOW_JAVA_COMMENTS, true)
.configure(ALLOW_SINGLE_QUOTES, true)
.configure(ALLOW_TRAILING_COMMA, true)
.configure(ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true)
.configure(ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS, true)
.configure(ALLOW_UNESCAPED_CONTROL_CHARS, true)
.configure(FAIL_ON_MISSING_CREATOR_PROPERTIES, false)
.configure(ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
.configure(SORT_PROPERTIES_ALPHABETICALLY, false)
.addModule(JavaTimeModule())
.addModule(SimpleModule()
.addSerializer(ByteBuffer::class.java, ByteBufferSerializer)
.addDeserializer(ByteBuffer::class.java, ByteBufferDeserializer)
.addSerializer(ClosedRange::class.java, ClosedRangeSerializer)
.addDeserializer(ClosedRange::class.java, ClosedRangeDeserializer())
.addSerializer(Float::class.java, FloatSerializer)
.addDeserializer(Float::class.java, FloatDeserializer)
.addSerializer(InetAddress::class.java, InetAddressSerializer)
.addDeserializer(InetAddress::class.java, InetAddressDeserializer)
)
.build()
fun nodeToCollection(node: JsonNode): Any? =
when (node) {
is ArrayNode -> node.toList().map { nodeToCollection(it) }
is ObjectNode -> {
var map = emptyMap<String, Any?>()
for (f in node.fields())
map = map + (f.key to nodeToCollection(f.value))
map
}
is TextNode -> node.textValue()
is BigIntegerNode -> node.bigIntegerValue()
is BooleanNode -> node.booleanValue()
is DecimalNode -> node.doubleValue()
is DoubleNode -> node.doubleValue()
is FloatNode -> node.floatValue()
is IntNode -> node.intValue()
is LongNode -> node.longValue()
is NullNode -> null
is BinaryNode -> node.binaryValue()
else -> error("Unknown node type: ${node::class.qualifiedName}")
}
fun mapNode(node: JsonNode): Any =
nodeToCollection(node) ?: error("Parsed content is 'null'")
object InetAddressSerializer : JsonSerializer<InetAddress>() {
override fun serialize(
value: InetAddress, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(value.hostAddress)
}
}
object InetAddressDeserializer : JsonDeserializer<InetAddress>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): InetAddress =
InetAddress.getByName(p.text)
}
object FloatSerializer : JsonSerializer<Float>() {
override fun serialize(value: Float, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeNumber(value.toBigDecimal().toDouble()) // BigDecimal needed for good rounding
}
}
object FloatDeserializer : JsonDeserializer<Float>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Float = p.floatValue
}
object ByteBufferSerializer: JsonSerializer<ByteBuffer>() {
override fun serialize(
value: ByteBuffer, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(Base64.getEncoder ().encodeToString (value.array()))
}
}
object ByteBufferDeserializer: JsonDeserializer<ByteBuffer>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ByteBuffer =
ByteBuffer.wrap (Base64.getDecoder ().decode (p.text))
}
object ClosedRangeSerializer: JsonSerializer<ClosedRange<*>> () {
override fun serialize(
value: ClosedRange<*>, gen: JsonGenerator, serializers: SerializerProvider) {
val start = value.start
val end = value.endInclusive
val valueSerializer = serializers.findValueSerializer(start.javaClass)
gen.writeStartObject()
gen.writeFieldName("start")
valueSerializer.serialize(start, gen, serializers)
gen.writeFieldName("endInclusive")
valueSerializer.serialize(end, gen, serializers)
gen.writeEndObject()
}
}
class ClosedRangeDeserializer(private val type: JavaType? = null) :
JsonDeserializer<ClosedRange<*>> (), ContextualDeserializer {
override fun createContextual(
ctxt: DeserializationContext, property: BeanProperty): JsonDeserializer<*> =
ClosedRangeDeserializer(property.type.containedType(0))
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ClosedRange<*> {
val token = p.currentToken
check (token == START_OBJECT) { "${token.name} should be: ${START_OBJECT.name}" }
check(p.nextFieldName() == "start") { "Ranges start with 'start' field" }
p.nextToken() // Start object
val start = ctxt.readValue<Comparable<Any>>(p, type)
check(p.nextFieldName() == "endInclusive") { "Ranges end with 'endInclusive' field" }
p.nextToken() // End array
val end = ctxt.readValue<Comparable<Any>>(p, type)
p.nextToken() // End array
return start .. end
}
}
}