Skip to content

Commit bc5e9be

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 c79bf46 commit bc5e9be

14 files changed

+57
-52
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
380380
> [!CAUTION]
381381
> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
382382
383+
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.
384+
383385
## Network options
384386

385387
### Retries

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

orb-java-core/src/main/kotlin/com/withorb/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
}

orb-java-core/src/main/kotlin/com/withorb/api/models/CustomerCreditLedgerCreateEntryByExternalIdParams.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2411,7 +2411,7 @@ private constructor(
24112411
return when (bestMatches.size) {
24122412
// This can happen if what we're deserializing is completely
24132413
// incompatible with all the possible variants (e.g. deserializing
2414-
// from object).
2414+
// from boolean).
24152415
0 -> CustomDueDate(_json = json)
24162416
1 -> bestMatches.single()
24172417
// If there's more than one match with the highest validity, then
@@ -2592,7 +2592,7 @@ private constructor(
25922592
return when (bestMatches.size) {
25932593
// This can happen if what we're deserializing is completely
25942594
// incompatible with all the possible variants (e.g. deserializing
2595-
// from object).
2595+
// from boolean).
25962596
0 -> InvoiceDate(_json = json)
25972597
1 -> bestMatches.single()
25982598
// If there's more than one match with the highest validity, then

orb-java-core/src/main/kotlin/com/withorb/api/models/CustomerCreditLedgerCreateEntryParams.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2403,7 +2403,7 @@ private constructor(
24032403
return when (bestMatches.size) {
24042404
// This can happen if what we're deserializing is completely
24052405
// incompatible with all the possible variants (e.g. deserializing
2406-
// from object).
2406+
// from boolean).
24072407
0 -> CustomDueDate(_json = json)
24082408
1 -> bestMatches.single()
24092409
// If there's more than one match with the highest validity, then
@@ -2584,7 +2584,7 @@ private constructor(
25842584
return when (bestMatches.size) {
25852585
// This can happen if what we're deserializing is completely
25862586
// incompatible with all the possible variants (e.g. deserializing
2587-
// from object).
2587+
// from boolean).
25882588
0 -> InvoiceDate(_json = json)
25892589
1 -> bestMatches.single()
25902590
// If there's more than one match with the highest validity, then

orb-java-core/src/main/kotlin/com/withorb/api/models/InvoiceCreateParams.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2085,7 +2085,7 @@ private constructor(
20852085
.toList()
20862086
return when (bestMatches.size) {
20872087
// This can happen if what we're deserializing is completely incompatible with
2088-
// all the possible variants (e.g. deserializing from object).
2088+
// all the possible variants (e.g. deserializing from boolean).
20892089
0 -> DueDate(_json = json)
20902090
1 -> bestMatches.single()
20912091
// If there's more than one match with the highest validity, then use the first

orb-java-core/src/main/kotlin/com/withorb/api/models/InvoiceUpdateParams.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ private constructor(
862862
.toList()
863863
return when (bestMatches.size) {
864864
// This can happen if what we're deserializing is completely incompatible with
865-
// all the possible variants (e.g. deserializing from object).
865+
// all the possible variants (e.g. deserializing from boolean).
866866
0 -> DueDate(_json = json)
867867
1 -> bestMatches.single()
868868
// If there's more than one match with the highest validity, then use the first
@@ -1033,7 +1033,7 @@ private constructor(
10331033
.toList()
10341034
return when (bestMatches.size) {
10351035
// This can happen if what we're deserializing is completely incompatible with
1036-
// all the possible variants (e.g. deserializing from object).
1036+
// all the possible variants (e.g. deserializing from boolean).
10371037
0 -> InvoiceDate(_json = json)
10381038
1 -> bestMatches.single()
10391039
// If there's more than one match with the highest validity, then use the first

orb-java-core/src/main/kotlin/com/withorb/api/models/PlanMigrationCancelResponse.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ private constructor(
460460
.toList()
461461
return when (bestMatches.size) {
462462
// This can happen if what we're deserializing is completely incompatible with
463-
// all the possible variants (e.g. deserializing from object).
463+
// all the possible variants (e.g. deserializing from boolean).
464464
0 -> EffectiveTime(_json = json)
465465
1 -> bestMatches.single()
466466
// If there's more than one match with the highest validity, then use the first

orb-java-core/src/main/kotlin/com/withorb/api/models/PlanMigrationListResponse.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ private constructor(
460460
.toList()
461461
return when (bestMatches.size) {
462462
// This can happen if what we're deserializing is completely incompatible with
463-
// all the possible variants (e.g. deserializing from object).
463+
// all the possible variants (e.g. deserializing from boolean).
464464
0 -> EffectiveTime(_json = json)
465465
1 -> bestMatches.single()
466466
// If there's more than one match with the highest validity, then use the first

orb-java-core/src/main/kotlin/com/withorb/api/models/PlanMigrationRetrieveResponse.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ private constructor(
461461
.toList()
462462
return when (bestMatches.size) {
463463
// This can happen if what we're deserializing is completely incompatible with
464-
// all the possible variants (e.g. deserializing from object).
464+
// all the possible variants (e.g. deserializing from boolean).
465465
0 -> EffectiveTime(_json = json)
466466
1 -> bestMatches.single()
467467
// If there's more than one match with the highest validity, then use the first

0 commit comments

Comments
 (0)