Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package io.github.projectmapk.jackson.module.kogera.ser.serializers

import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser.NumberType
import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper
import com.fasterxml.jackson.databind.module.SimpleSerializers
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE
import io.github.projectmapk.jackson.module.kogera.GenericValueClassUnboxConverter
Expand All @@ -29,28 +32,37 @@ import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.math.BigInteger

internal object UByteSerializer : StdSerializer<UByte>(UByte::class.java) {
// The unsigned integer serializers extend StdScalarSerializer so that polymorphic type information
// (e.g. default typing on an `Any` property) is written as for other scalar values,
// and report the integer format to schema generators (StdScalarSerializer assumes a string by default).
internal object UByteSerializer : StdScalarSerializer<UByte>(UByte::class.java) {
private fun readResolve(): Any = UByteSerializer

override fun serialize(value: UByte, gen: JsonGenerator, provider: SerializerProvider) = gen
.writeNumber(value.toShort())

override fun acceptJsonFormatVisitor(visitor: JsonFormatVisitorWrapper, typeHint: JavaType?) = visitIntFormat(visitor, typeHint, NumberType.INT)
}

internal object UShortSerializer : StdSerializer<UShort>(UShort::class.java) {
internal object UShortSerializer : StdScalarSerializer<UShort>(UShort::class.java) {
private fun readResolve(): Any = UShortSerializer

override fun serialize(value: UShort, gen: JsonGenerator, provider: SerializerProvider) = gen
.writeNumber(value.toInt())

override fun acceptJsonFormatVisitor(visitor: JsonFormatVisitorWrapper, typeHint: JavaType?) = visitIntFormat(visitor, typeHint, NumberType.INT)
}

internal object UIntSerializer : StdSerializer<UInt>(UInt::class.java) {
internal object UIntSerializer : StdScalarSerializer<UInt>(UInt::class.java) {
private fun readResolve(): Any = UIntSerializer

override fun serialize(value: UInt, gen: JsonGenerator, provider: SerializerProvider) = gen
.writeNumber(value.toLong())

override fun acceptJsonFormatVisitor(visitor: JsonFormatVisitorWrapper, typeHint: JavaType?) = visitIntFormat(visitor, typeHint, NumberType.LONG)
}

internal object ULongSerializer : StdSerializer<ULong>(ULong::class.java) {
internal object ULongSerializer : StdScalarSerializer<ULong>(ULong::class.java) {
private fun readResolve(): Any = ULongSerializer

override fun serialize(value: ULong, gen: JsonGenerator, provider: SerializerProvider) {
Expand All @@ -60,6 +72,8 @@ internal object ULongSerializer : StdSerializer<ULong>(ULong::class.java) {
else -> gen.writeNumber(BigInteger(value.toString()))
}
}

override fun acceptJsonFormatVisitor(visitor: JsonFormatVisitorWrapper, typeHint: JavaType?) = visitIntFormat(visitor, typeHint, NumberType.BIG_INTEGER)
}

// Class must be UnboxableValueClass.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.github.projectmapk.jackson.module.kogera.zPorted.test

import com.fasterxml.jackson.core.JsonParser.NumberType
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonAnyFormatVisitor
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor
import io.github.projectmapk.jackson.module.kogera.defaultMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

/**
* The unsigned integer serializers report the integer format to schema generators
* (previously "any", the StdSerializer default).
*/
internal class UnsignedNumbersJsonFormatVisitorTest {
private class RecordingVisitor : JsonFormatVisitorWrapper.Base() {
var format: String? = null
var numberType: NumberType? = null

override fun expectIntegerFormat(type: JavaType): JsonIntegerFormatVisitor {
format = "integer"

return object : JsonIntegerFormatVisitor.Base() {
override fun numberType(type: NumberType) {
numberType = type
}
}
}

override fun expectAnyFormat(type: JavaType): JsonAnyFormatVisitor? {
format = "any"
return super.expectAnyFormat(type)
}

override fun expectStringFormat(type: JavaType): JsonStringFormatVisitor? {
format = "string"
return super.expectStringFormat(type)
}
}

private fun visit(type: Class<*>): RecordingVisitor =
RecordingVisitor().also { defaultMapper.acceptJsonFormatVisitor(type, it) }

private fun assertInteger(type: Class<*>, numberType: NumberType) {
val visitor = visit(type)

assertEquals("integer", visitor.format)
assertEquals(numberType, visitor.numberType)
}

@Test
fun `UByte is an integer`() = assertInteger(UByte::class.java, NumberType.INT)

@Test
fun `UShort is an integer`() = assertInteger(UShort::class.java, NumberType.INT)

@Test
fun `UInt is an integer`() = assertInteger(UInt::class.java, NumberType.LONG)

@Test
fun `ULong is an integer`() = assertInteger(ULong::class.java, NumberType.BIG_INTEGER)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package io.github.projectmapk.jackson.module.kogera.zPorted.test

import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonTypeInfo.As
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator
import io.github.projectmapk.jackson.module.kogera.defaultMapper
import io.github.projectmapk.jackson.module.kogera.jacksonMapperBuilder
import io.github.projectmapk.jackson.module.kogera.readValue
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

/**
* Unsigned numbers stored in a position that requires polymorphic type information
* (an `Any` property under default typing, or a property annotated with `@JsonTypeInfo`)
* are written with a type id, as other scalar values are, e.g. `["kotlin.UInt",1]`.
*
* Reading such input already worked before, so the round-trip tests check that
* the written form is the one the deserializers expect.
*/
internal class UnsignedNumbersTypeInfoTest {
data class AnyHolder(val value: Any)

data class AnnotatedHolder(@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) val value: Any)

data class AnnotatedWrapperObjectHolder(
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = As.WRAPPER_OBJECT) val value: Any,
)

private val typeValidator = BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build()

private val defaultTypingMapper = jacksonMapperBuilder()
.activateDefaultTyping(typeValidator, DefaultTyping.OBJECT_AND_NON_CONCRETE, As.PROPERTY)
.build()

// The default validator denies `Any` as base type, so one that allows it is needed for the `Any` properties.
private val annotatedMapper = jacksonMapperBuilder().polymorphicTypeValidator(typeValidator).build()

abstract inner class ForType<T : Any>(private val value: T, private val json: String) {
private val id = value::class.java.name

@Test
fun `default typing - type id is written`() {
assertEquals("""{"value":["$id",$json]}""", defaultTypingMapper.writeValueAsString(AnyHolder(value)))
}

@Test
fun `default typing - round trip`() {
val expected = AnyHolder(value)
val json = defaultTypingMapper.writeValueAsString(expected)

assertEquals(expected, defaultTypingMapper.readValue<AnyHolder>(json))
}

@Test
fun `JsonTypeInfo as property - type id is written`() {
assertEquals("""{"value":["$id",$json]}""", annotatedMapper.writeValueAsString(AnnotatedHolder(value)))
}

@Test
fun `JsonTypeInfo as property - round trip`() {
val expected = AnnotatedHolder(value)
val json = annotatedMapper.writeValueAsString(expected)

assertEquals(expected, annotatedMapper.readValue<AnnotatedHolder>(json))
}

@Test
fun `JsonTypeInfo as wrapper object - type id is written`() {
assertEquals(
"""{"value":{"$id":$json}}""",
annotatedMapper.writeValueAsString(AnnotatedWrapperObjectHolder(value)),
)
}

@Test
fun `JsonTypeInfo as wrapper object - round trip`() {
val expected = AnnotatedWrapperObjectHolder(value)
val json = annotatedMapper.writeValueAsString(expected)

assertEquals(expected, annotatedMapper.readValue<AnnotatedWrapperObjectHolder>(json))
}
}

@Nested
inner class ForUByte : ForType<UByte>(UByte.MAX_VALUE, "255")

@Nested
inner class ForUShort : ForType<UShort>(UShort.MAX_VALUE, "65535")

@Nested
inner class ForUInt : ForType<UInt>(UInt.MAX_VALUE, "4294967295")

@Nested
inner class ForULong : ForType<ULong>(ULong.MAX_VALUE, "18446744073709551615")

data class AnnotatedListHolder(@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) val values: List<UInt>)

// @JsonTypeInfo on a collection property applies to its contents,
// so here the serializer is selected by the declared unsigned type rather than by the runtime type.
@Test
fun `JsonTypeInfo on collection contents`() {
val expected = AnnotatedListHolder(listOf(1u, UInt.MAX_VALUE))
val json = annotatedMapper.writeValueAsString(expected)

assertEquals("""{"values":[["kotlin.UInt",1],["kotlin.UInt",4294967295]]}""", json)
assertEquals(expected, annotatedMapper.readValue<AnnotatedListHolder>(json))
}

data class DeclaredHolder(val value: UInt, val nullable: UInt?)

// The unsigned types are final, so default typing other than EVERYTHING
// does not apply to properties declared with them.
@Test
fun `not applied to declared properties under NON_FINAL`() {
val mapper = jacksonMapperBuilder()
.activateDefaultTyping(typeValidator, DefaultTyping.NON_FINAL, As.PROPERTY)
.build()
val expected = DeclaredHolder(1u, 2u)
val json = mapper.writeValueAsString(expected)

assertEquals("""{"value":1,"nullable":2}""", json)
assertEquals(expected, mapper.readValue<DeclaredHolder>(json))
}

data class NullableHolder(val nullable: UInt?)

// A nullable unsigned property is boxed on the JVM, so EVERYTHING applies type information to it.
// EVERYTHING also adds a type id to the root object, so only the property is compared.
@Test
fun `applied to nullable declared property under EVERYTHING`() {
val mapper = jacksonMapperBuilder()
.activateDefaultTyping(typeValidator, DefaultTyping.EVERYTHING, As.PROPERTY)
.build()
val expected = NullableHolder(2u)
val json = mapper.writeValueAsString(expected)

assertEquals(defaultMapper.readTree("""["kotlin.UInt",2]"""), defaultMapper.readTree(json)["nullable"])
assertEquals(expected, mapper.readValue<NullableHolder>(json))
}
}
Loading