Skip to content

Commit 248d372

Browse files
authored
Merge pull request #154 from ProjectMapK/feat/json-unboxed
Added `JsonUnbox` annotation to improve `value class` serialization performance
2 parents 9b3e42e + cf0ced9 commit 248d372

File tree

6 files changed

+122
-1
lines changed

6 files changed

+122
-1
lines changed

docs/AboutValueClassSupport.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ The same policy applies to deserialization.
4343
This policy was decided with reference to the behavior as of `jackson-module-kotlin 2.14.1` and [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/value-classes.md#serializable-value-classes).
4444
However, these are just basic policies, and the behavior can be overridden with `JsonSerializer` or `JsonDeserializer`.
4545

46+
### Serialization performance improvement using `JsonUnbox`
47+
In `jackson-module-kogera`, the `jackson` functionality is modified by reflection so that the `Jackson` functionality works for `value class` as well.
48+
These are executed on all calls.
49+
On the other hand, if only `unbox` is required, these are extra overheads that can significantly impair serialization performance.
50+
51+
Therefore, `jackson-module-kogera` provides the `JsonUnbox` annotation.
52+
When this annotation is provided, serialization performance is improved because only calls to `getter` that is `unboxed` will be made.
53+
4654
## For features that would not be supported
4755
I do not intend to support features that require significant effort to support and that can be circumvented by user definition.
4856

docs/KogeraSpecificImplementations.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ I will consider making the classes public again when we receive requests for the
2525
## Remove old options and `deprecated` code
2626
Because `jackson-module-kotlin` is a framework with a long history, some old code and options remain.
2727
In `kogera`, those options have been removed.
28+
29+
# Value class support
30+
The `jackson-module-kogera` supports many use cases of `value class` (`inline class`).
31+
This is summarized [here](./AboutValueClassSupport.md).
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.projectmapk.jackson.module.kogera.annotation
2+
3+
/**
4+
* Only affects properties that return value class and value class.
5+
* If given, parsing to apply `Jackson` features will be skipped and only unbox will be performed.
6+
* This will prevent various features such as JsonValue and custom serializers from working,
7+
* but will improve serialization performance.
8+
*/
9+
@Retention(AnnotationRetention.RUNTIME)
10+
@MustBeDocumented
11+
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY_GETTER)
12+
public annotation class JsonUnbox

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotation_introspector/KotlinFallbackAnnotationIntrospector.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.util.Converter
1212
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
1313
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
1414
import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter
15+
import io.github.projectmapk.jackson.module.kogera.annotation.JsonUnbox
1516
import io.github.projectmapk.jackson.module.kogera.deser.CollectionValueStrictNullChecksConverter
1617
import io.github.projectmapk.jackson.module.kogera.deser.MapValueStrictNullChecksConverter
1718
import io.github.projectmapk.jackson.module.kogera.isNullable
@@ -83,7 +84,12 @@ internal class KotlinFallbackAnnotationIntrospector(
8384
KotlinDurationValueToJavaDurationConverter
8485
}
8586
} else {
86-
cache.getValueClassBoxConverter(a.rawReturnType, it)
87+
// If JsonUnbox is specified, the unboxed getter is used as is.
88+
if (a.hasAnnotation(JsonUnbox::class.java) || it.getAnnotation(JsonUnbox::class.java) != null) {
89+
null
90+
} else {
91+
cache.getValueClassBoxConverter(a.rawReturnType, it)
92+
}
8793
}
8894
}
8995
is AnnotatedClass -> lookupKotlinTypeConverter(a)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.github.projectmapk.jackson.module.kogera._integration.ser.value_class.json_unbox
2+
3+
import io.github.projectmapk.jackson.module.kogera.annotation.JsonUnbox
4+
import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper
5+
import org.junit.jupiter.api.Assertions.assertEquals
6+
import org.junit.jupiter.api.Test
7+
8+
class ForClass {
9+
@JvmInline
10+
@JsonUnbox
11+
value class Primitive(val v: Int)
12+
13+
@JvmInline
14+
@JsonUnbox
15+
value class NonNullObject(val v: String)
16+
17+
@JvmInline
18+
@JsonUnbox
19+
value class NullableObject(val v: String?)
20+
21+
data class Dto(
22+
val p0: Primitive = Primitive(0),
23+
val p1: Primitive? = Primitive(1),
24+
val p2: Primitive? = null,
25+
val nno0: NonNullObject = NonNullObject("0"),
26+
val nno1: NonNullObject? = NonNullObject("1"),
27+
val nno2: NonNullObject? = null,
28+
val no0: NullableObject = NullableObject("0"),
29+
val no1: NullableObject = NullableObject(null),
30+
val no2: NullableObject? = NullableObject("2"),
31+
val no3: NullableObject? = null
32+
)
33+
34+
@Test
35+
fun test() {
36+
val expected =
37+
"""{"p0":0,"p1":1,"p2":null,"nno0":"0","nno1":"1","nno2":null,"no0":"0","no1":null,"no2":"2","no3":null}"""
38+
val actual = jacksonObjectMapper().writeValueAsString(Dto())
39+
40+
assertEquals(expected, actual)
41+
}
42+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package io.github.projectmapk.jackson.module.kogera._integration.ser.value_class.json_unbox
2+
3+
import io.github.projectmapk.jackson.module.kogera.annotation.JsonUnbox
4+
import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper
5+
import org.junit.jupiter.api.Assertions.assertEquals
6+
import org.junit.jupiter.api.Test
7+
8+
class ForProperty {
9+
@JvmInline
10+
value class Primitive(val v: Int)
11+
12+
@JvmInline
13+
value class NonNullObject(val v: String)
14+
15+
@JvmInline
16+
value class NullableObject(val v: String?)
17+
18+
data class Dto(
19+
@get:JsonUnbox
20+
val p0: Primitive = Primitive(0),
21+
@get:JsonUnbox
22+
val p1: Primitive? = Primitive(1),
23+
@get:JsonUnbox
24+
val p2: Primitive? = null,
25+
@get:JsonUnbox
26+
val nno0: NonNullObject = NonNullObject("0"),
27+
@get:JsonUnbox
28+
val nno1: NonNullObject? = NonNullObject("1"),
29+
@get:JsonUnbox
30+
val nno2: NonNullObject? = null,
31+
@get:JsonUnbox
32+
val no0: NullableObject = NullableObject("0"),
33+
@get:JsonUnbox
34+
val no1: NullableObject = NullableObject(null),
35+
@get:JsonUnbox
36+
val no2: NullableObject? = NullableObject("2"),
37+
@get:JsonUnbox
38+
val no3: NullableObject? = null
39+
)
40+
41+
@Test
42+
fun test() {
43+
val expected =
44+
"""{"p0":0,"p1":1,"p2":null,"nno0":"0","nno1":"1","nno2":null,"no0":"0","no1":null,"no2":"2","no3":null}"""
45+
val actual = jacksonObjectMapper().writeValueAsString(Dto())
46+
47+
assertEquals(expected, actual)
48+
}
49+
}

0 commit comments

Comments
 (0)