Skip to content

Commit a651bdc

Browse files
authored
chore: simplify TristateModule type-resolution and serializer wiring (#184)
PR: #184
1 parent c291ac0 commit a651bdc

2 files changed

Lines changed: 29 additions & 24 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
package org.dexpace.sdk.serde.jackson
9+
10+
import com.fasterxml.jackson.databind.JavaType
11+
import com.fasterxml.jackson.databind.type.TypeFactory
12+
import org.dexpace.sdk.core.serde.Tristate
13+
14+
/** The element [JavaType] for `Object` — the fallback when a `Tristate` is raw or `Tristate<*>`. */
15+
internal val ANY_TYPE: JavaType = TypeFactory.defaultInstance().constructType(Any::class.java)
16+
17+
/** The first contained type argument, or `null` when the type is raw / carries no parameters. */
18+
internal fun JavaType.firstContainedOrNull(): JavaType? = if (containedTypeCount() > 0) containedType(0) else null
19+
20+
/** Whether this type is (or extends) [Tristate]. */
21+
internal fun JavaType.isTristate(): Boolean = Tristate::class.java.isAssignableFrom(rawClass)

sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/TristateModule.kt

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ import com.fasterxml.jackson.databind.deser.Deserializers
2424
import com.fasterxml.jackson.databind.module.SimpleModule
2525
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter
2626
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier
27-
import com.fasterxml.jackson.databind.ser.ContextualSerializer
2827
import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap
29-
import com.fasterxml.jackson.databind.type.TypeFactory
3028
import org.dexpace.sdk.core.serde.Tristate
3129

3230
/**
@@ -88,15 +86,9 @@ internal class TristateDeserializers internal constructor() : Deserializers.Base
8886
config: DeserializationConfig,
8987
beanDesc: BeanDescription,
9088
): JsonDeserializer<*>? =
91-
if (Tristate::class.java.isAssignableFrom(type.rawClass)) {
89+
if (type.isTristate()) {
9290
// Inner type defaults to Object when the user wrote `Tristate<*>` or raw `Tristate`.
93-
val inner: JavaType =
94-
if (type.containedTypeCount() > 0) {
95-
type.containedType(0)
96-
} else {
97-
TypeFactory.defaultInstance().constructType(Any::class.java)
98-
}
99-
TristateDeserializer(inner)
91+
TristateDeserializer(type.firstContainedOrNull() ?: ANY_TYPE)
10092
} else {
10193
null
10294
}
@@ -114,9 +106,8 @@ internal class TristateDeserializer internal constructor(
114106
// `Tristate<T>`. Falling back to the constructor-provided innerType lets callers that
115107
// deserialize Tristate directly (without a wrapping bean) still get correct typing.
116108
val resolved: JavaType =
117-
property?.type?.takeIf { Tristate::class.java.isAssignableFrom(it.rawClass) }?.let { wrapper ->
118-
if (wrapper.containedTypeCount() > 0) wrapper.containedType(0) else null
119-
} ?: innerType ?: TypeFactory.defaultInstance().constructType(Any::class.java)
109+
property?.type?.takeIf { it.isTristate() }?.firstContainedOrNull()
110+
?: innerType ?: ANY_TYPE
120111
return TristateDeserializer(resolved)
121112
}
122113

@@ -127,7 +118,7 @@ internal class TristateDeserializer internal constructor(
127118
if (p.currentToken() == JsonToken.VALUE_NULL) {
128119
return Tristate.Null
129120
}
130-
val target = innerType ?: TypeFactory.defaultInstance().constructType(Any::class.java)
121+
val target = innerType ?: ANY_TYPE
131122
val value: Any? = ctxt.readValue(p, target)
132123
return if (value == null) Tristate.Null else Tristate.Present(value)
133124
}
@@ -155,18 +146,13 @@ internal class TristateDeserializer internal constructor(
155146
* implementation writes `null` for both Absent and Null — JSON itself has no "field is
156147
* missing" concept when there's no enclosing object to omit a key from.
157148
*/
158-
internal class TristateSerializer internal constructor() : JsonSerializer<Tristate<*>>(), ContextualSerializer {
149+
internal class TristateSerializer internal constructor() : JsonSerializer<Tristate<*>>() {
159150
// Cached per-type sub-serializer lookup so repeated calls with the same T don't pay
160151
// ObjectMapper lookup cost. PropertySerializerMap.findAndAddPrimarySerializer is the
161152
// canonical Jackson idiom for this.
162153
@Volatile
163154
private var dynamicSerializers: PropertySerializerMap = PropertySerializerMap.emptyForProperties()
164155

165-
override fun createContextual(
166-
prov: SerializerProvider,
167-
property: BeanProperty?,
168-
): JsonSerializer<*> = this
169-
170156
override fun serialize(
171157
value: Tristate<*>,
172158
gen: JsonGenerator,
@@ -225,10 +211,8 @@ internal class TristateSerializerModifier internal constructor() : BeanSerialize
225211
beanDesc: BeanDescription,
226212
beanProperties: MutableList<BeanPropertyWriter>,
227213
): MutableList<BeanPropertyWriter> {
228-
beanProperties.forEachIndexed { i, writer ->
229-
if (Tristate::class.java.isAssignableFrom(writer.type.rawClass)) {
230-
beanProperties[i] = TristatePropertyWriter(writer)
231-
}
214+
beanProperties.replaceAll { writer ->
215+
if (writer.type.isTristate()) TristatePropertyWriter(writer) else writer
232216
}
233217
return beanProperties
234218
}

0 commit comments

Comments
 (0)