Skip to content

Commit fcc607b

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 eaa7933 commit fcc607b

7 files changed

Lines changed: 41 additions & 36 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
393393
> [!CAUTION]
394394
> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
395395
396+
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.
397+
396398
## Network options
397399

398400
### Retries

finch-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

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

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import java.io.InputStream
2424
import java.time.DateTimeException
2525
import java.time.LocalDate
2626
import java.time.LocalDateTime
27+
import java.time.OffsetDateTime
2728
import java.time.ZonedDateTime
2829
import java.time.format.DateTimeFormatter
2930
import java.time.temporal.ChronoField
@@ -36,7 +37,7 @@ fun jsonMapper(): JsonMapper =
3637
.addModule(
3738
SimpleModule()
3839
.addSerializer(InputStreamSerializer)
39-
.addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
40+
.addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer())
4041
)
4142
.withCoercionConfig(LogicalType.Boolean) {
4243
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
@@ -64,6 +65,12 @@ fun jsonMapper(): JsonMapper =
6465
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
6566
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
6667
}
68+
.withCoercionConfig(LogicalType.DateTime) {
69+
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
70+
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
71+
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
72+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
73+
}
6774
.withCoercionConfig(LogicalType.Array) {
6875
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
6976
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
@@ -124,10 +131,10 @@ private object InputStreamSerializer : BaseSerializer<InputStream>(InputStream::
124131
}
125132

126133
/**
127-
* A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
134+
* A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes.
128135
*/
129-
private class LenientLocalDateTimeDeserializer :
130-
StdDeserializer<LocalDateTime>(LocalDateTime::class.java) {
136+
private class LenientOffsetDateTimeDeserializer :
137+
StdDeserializer<OffsetDateTime>(OffsetDateTime::class.java) {
131138

132139
companion object {
133140

@@ -141,26 +148,28 @@ private class LenientLocalDateTimeDeserializer :
141148

142149
override fun logicalType(): LogicalType = LogicalType.DateTime
143150

144-
override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
151+
override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime {
145152
val exceptions = mutableListOf<Exception>()
146153

147154
for (formatter in DATE_TIME_FORMATTERS) {
148155
try {
149156
val temporal = formatter.parse(p.text)
150157

151158
return when {
152-
!temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
153-
LocalDate.from(temporal).atStartOfDay()
154-
!temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
155-
LocalDateTime.from(temporal)
156-
else -> ZonedDateTime.from(temporal).toLocalDateTime()
157-
}
159+
!temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
160+
LocalDate.from(temporal).atStartOfDay()
161+
!temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
162+
LocalDateTime.from(temporal)
163+
else -> ZonedDateTime.from(temporal).toLocalDateTime()
164+
}
165+
.atZone(context.timeZone.toZoneId())
166+
.toOffsetDateTime()
158167
} catch (e: DateTimeException) {
159168
exceptions.add(e)
160169
}
161170
}
162171

163-
throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
172+
throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply {
164173
exceptions.forEach { addSuppressed(it) }
165174
}
166175
}

finch-java-core/src/main/kotlin/com/tryfinch/api/models/Introspection.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,7 +1465,7 @@ private constructor(
14651465
.toList()
14661466
return when (bestMatches.size) {
14671467
// This can happen if what we're deserializing is completely incompatible
1468-
// with all the possible variants (e.g. deserializing from object).
1468+
// with all the possible variants (e.g. deserializing from boolean).
14691469
0 -> LastSuccessfulSync(_json = json)
14701470
1 -> bestMatches.single()
14711471
// If there's more than one match with the highest validity, then use the
@@ -2429,7 +2429,7 @@ private constructor(
24292429
return when (bestMatches.size) {
24302430
// This can happen if what we're deserializing is completely
24312431
// incompatible with all the possible variants (e.g. deserializing from
2432-
// object).
2432+
// boolean).
24332433
0 -> LastSuccessfulSync(_json = json)
24342434
1 -> bestMatches.single()
24352435
// If there's more than one match with the highest validity, then use

finch-java-core/src/main/kotlin/com/tryfinch/api/models/RequestForwardingForwardResponse.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ private constructor(
746746
.toList()
747747
return when (bestMatches.size) {
748748
// This can happen if what we're deserializing is completely incompatible
749-
// with all the possible variants (e.g. deserializing from array).
749+
// with all the possible variants (e.g. deserializing from boolean).
750750
0 -> Data(_json = json)
751751
1 -> bestMatches.single()
752752
// If there's more than one match with the highest validity, then use the

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package com.tryfinch.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
6+
import java.time.OffsetDateTime
77
import kotlin.reflect.KClass
88
import org.assertj.core.api.Assertions.assertThat
99
import org.assertj.core.api.Assertions.catchThrowable
@@ -58,14 +58,6 @@ internal class ObjectMappersTest {
5858
LONG to DOUBLE,
5959
LONG to INTEGER,
6060
CLASS to MAP,
61-
// These aren't actually valid, but coercion configs don't work for String until
62-
// v2.14.0: https://github.com/FasterXML/jackson-databind/issues/3240
63-
// We currently test on v2.13.4.
64-
BOOLEAN to STRING,
65-
FLOAT to STRING,
66-
DOUBLE to STRING,
67-
INTEGER to STRING,
68-
LONG to STRING,
6961
)
7062
}
7163
}
@@ -84,7 +76,7 @@ internal class ObjectMappersTest {
8476
}
8577
}
8678

87-
enum class LenientLocalDateTimeTestCase(val string: String) {
79+
enum class LenientOffsetDateTimeTestCase(val string: String) {
8880
DATE("1998-04-21"),
8981
DATE_TIME("1998-04-21T04:00:00"),
9082
ZONED_DATE_TIME_1("1998-04-21T04:00:00+03:00"),
@@ -93,10 +85,10 @@ internal class ObjectMappersTest {
9385

9486
@ParameterizedTest
9587
@EnumSource
96-
fun readLocalDateTime_lenient(testCase: LenientLocalDateTimeTestCase) {
88+
fun readOffsetDateTime_lenient(testCase: LenientOffsetDateTimeTestCase) {
9789
val jsonMapper = jsonMapper()
9890
val json = jsonMapper.writeValueAsString(testCase.string)
9991

100-
assertDoesNotThrow { jsonMapper().readValue<LocalDateTime>(json) }
92+
assertDoesNotThrow { jsonMapper().readValue<OffsetDateTime>(json) }
10193
}
10294
}

finch-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)