Skip to content

Commit ad467a4

Browse files
chore: test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind#3240 in tests
fix: date time deserialization leniency
1 parent 7838ba8 commit ad467a4

File tree

8 files changed

+51
-39
lines changed

8 files changed

+51
-39
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
416416
> [!CAUTION]
417417
> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
418418
419+
Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead.
420+
419421
## Network options
420422

421423
### Retries

lithic-java-core/build.gradle.kts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ plugins {
55

66
configurations.all {
77
resolutionStrategy {
8-
// Compile and test against a lower Jackson version to ensure we're compatible with it.
9-
// We publish with a higher version (see below) to ensure users depend on a secure version by default.
10-
force("com.fasterxml.jackson.core:jackson-core:2.13.4")
11-
force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
12-
force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
13-
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
14-
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
15-
force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
8+
// Compile and test against a lower Jackson version to ensure we're compatible with it. Note that
9+
// we generally support 2.13.4, but test against 2.14.0 because 2.13.4 has some annoying (but
10+
// niche) bugs (users should upgrade if they encounter them). We publish with a higher version
11+
// (see below) to ensure users depend on a secure version by default.
12+
force("com.fasterxml.jackson.core:jackson-core:2.14.0")
13+
force("com.fasterxml.jackson.core:jackson-databind:2.14.0")
14+
force("com.fasterxml.jackson.core:jackson-annotations:2.14.0")
15+
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.0")
16+
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0")
17+
force("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0")
1618
}
1719
}
1820

lithic-java-core/src/main/kotlin/com/lithic/api/core/ObjectMappers.kt

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ fun jsonMapper(): JsonMapper =
3737
.addModule(
3838
SimpleModule()
3939
.addSerializer(InputStreamSerializer)
40-
.addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
40+
.addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer())
4141
.addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer())
4242
)
4343
.withCoercionConfig(LogicalType.Boolean) {
@@ -66,6 +66,12 @@ fun jsonMapper(): JsonMapper =
6666
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
6767
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
6868
}
69+
.withCoercionConfig(LogicalType.DateTime) {
70+
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
71+
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
72+
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
73+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
74+
}
6975
.withCoercionConfig(LogicalType.Array) {
7076
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
7177
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
@@ -126,10 +132,10 @@ private object InputStreamSerializer : BaseSerializer<InputStream>(InputStream::
126132
}
127133

128134
/**
129-
* A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
135+
* A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes.
130136
*/
131-
private class LenientLocalDateTimeDeserializer :
132-
StdDeserializer<LocalDateTime>(LocalDateTime::class.java) {
137+
private class LenientOffsetDateTimeDeserializer :
138+
StdDeserializer<OffsetDateTime>(OffsetDateTime::class.java) {
133139

134140
companion object {
135141

@@ -143,26 +149,28 @@ private class LenientLocalDateTimeDeserializer :
143149

144150
override fun logicalType(): LogicalType = LogicalType.DateTime
145151

146-
override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
152+
override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime {
147153
val exceptions = mutableListOf<Exception>()
148154

149155
for (formatter in DATE_TIME_FORMATTERS) {
150156
try {
151157
val temporal = formatter.parse(p.text)
152158

153159
return when {
154-
!temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
155-
LocalDate.from(temporal).atStartOfDay()
156-
!temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
157-
LocalDateTime.from(temporal)
158-
else -> ZonedDateTime.from(temporal).toLocalDateTime()
159-
}
160+
!temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
161+
LocalDate.from(temporal).atStartOfDay()
162+
!temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
163+
LocalDateTime.from(temporal)
164+
else -> ZonedDateTime.from(temporal).toLocalDateTime()
165+
}
166+
.atZone(context.timeZone.toZoneId())
167+
.toOffsetDateTime()
160168
} catch (e: DateTimeException) {
161169
exceptions.add(e)
162170
}
163171
}
164172

165-
throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
173+
throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply {
166174
exceptions.forEach { addSuppressed(it) }
167175
}
168176
}

lithic-java-core/src/main/kotlin/com/lithic/api/models/CardProvisionResponse.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ private constructor(
364364
.toList()
365365
return when (bestMatches.size) {
366366
// This can happen if what we're deserializing is completely incompatible with
367-
// all the possible variants (e.g. deserializing from array).
367+
// all the possible variants (e.g. deserializing from boolean).
368368
0 -> ProvisioningPayload(_json = json)
369369
1 -> bestMatches.single()
370370
// If there's more than one match with the highest validity, then use the first

lithic-java-core/src/main/kotlin/com/lithic/api/models/ConditionalValue.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ private constructor(
247247
.toList()
248248
return when (bestMatches.size) {
249249
// This can happen if what we're deserializing is completely incompatible with all
250-
// the possible variants (e.g. deserializing from object).
250+
// the possible variants (e.g. deserializing from boolean).
251251
0 -> ConditionalValue(_json = json)
252252
1 -> bestMatches.single()
253253
// If there's more than one match with the highest validity, then use the first

lithic-java-core/src/test/kotlin/com/lithic/api/core/ObjectMappersTest.kt

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.lithic.api.core
33
import com.fasterxml.jackson.annotation.JsonProperty
44
import com.fasterxml.jackson.databind.exc.MismatchedInputException
55
import com.fasterxml.jackson.module.kotlin.readValue
6-
import java.time.LocalDateTime
76
import java.time.OffsetDateTime
87
import kotlin.reflect.KClass
98
import org.assertj.core.api.Assertions.assertThat
@@ -59,14 +58,6 @@ internal class ObjectMappersTest {
5958
LONG to DOUBLE,
6059
LONG to INTEGER,
6160
CLASS to MAP,
62-
// These aren't actually valid, but coercion configs don't work for String until
63-
// v2.14.0: https://github.com/FasterXML/jackson-databind/issues/3240
64-
// We currently test on v2.13.4.
65-
BOOLEAN to STRING,
66-
FLOAT to STRING,
67-
DOUBLE to STRING,
68-
INTEGER to STRING,
69-
LONG to STRING,
7061
)
7162
}
7263
}
@@ -85,7 +76,7 @@ internal class ObjectMappersTest {
8576
}
8677
}
8778

88-
enum class LenientLocalDateTimeTestCase(val string: String) {
79+
enum class LenientOffsetDateTimeTestCase(val string: String) {
8980
DATE("1998-04-21"),
9081
DATE_TIME("1998-04-21T04:00:00"),
9182
ZONED_DATE_TIME_1("1998-04-21T04:00:00+03:00"),
@@ -94,11 +85,11 @@ internal class ObjectMappersTest {
9485

9586
@ParameterizedTest
9687
@EnumSource
97-
fun readLocalDateTime_lenient(testCase: LenientLocalDateTimeTestCase) {
88+
fun readOffsetDateTime_lenient(testCase: LenientOffsetDateTimeTestCase) {
9889
val jsonMapper = jsonMapper()
9990
val json = jsonMapper.writeValueAsString(testCase.string)
10091

101-
assertDoesNotThrow { jsonMapper().readValue<LocalDateTime>(json) }
92+
assertDoesNotThrow { jsonMapper().readValue<OffsetDateTime>(json) }
10293
}
10394

10495
enum class LenientOffsetDateTimeTestCase(val string: String) {

lithic-java-core/src/test/kotlin/com/lithic/api/models/ConditionalValueTest.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import java.time.OffsetDateTime
1010
import org.assertj.core.api.Assertions.assertThat
1111
import org.junit.jupiter.api.Test
1212
import org.junit.jupiter.api.assertThrows
13+
import org.junit.jupiter.params.ParameterizedTest
14+
import org.junit.jupiter.params.provider.EnumSource
1315

1416
internal class ConditionalValueTest {
1517

@@ -118,10 +120,17 @@ internal class ConditionalValueTest {
118120
assertThat(roundtrippedConditionalValue).isEqualTo(conditionalValue)
119121
}
120122

121-
@Test
122-
fun incompatibleJsonShapeDeserializesToUnknown() {
123-
val value = JsonValue.from(mapOf("invalid" to "object"))
124-
val conditionalValue = jsonMapper().convertValue(value, jacksonTypeRef<ConditionalValue>())
123+
enum class IncompatibleJsonShapeTestCase(val value: JsonValue) {
124+
BOOLEAN(JsonValue.from(false)),
125+
FLOAT(JsonValue.from(3.14)),
126+
OBJECT(JsonValue.from(mapOf("invalid" to "object"))),
127+
}
128+
129+
@ParameterizedTest
130+
@EnumSource
131+
fun incompatibleJsonShapeDeserializesToUnknown(testCase: IncompatibleJsonShapeTestCase) {
132+
val conditionalValue =
133+
jsonMapper().convertValue(testCase.value, jacksonTypeRef<ConditionalValue>())
125134

126135
val e = assertThrows<LithicInvalidDataException> { conditionalValue.validate() }
127136
assertThat(e).hasMessageStartingWith("Unknown ")

lithic-java-proguard-test/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dependencies {
1919
testImplementation(kotlin("test"))
2020
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
2121
testImplementation("org.assertj:assertj-core:3.25.3")
22-
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
22+
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0")
2323
}
2424

2525
tasks.shadowJar {

0 commit comments

Comments
 (0)