Skip to content

Commit 63d6c36

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 2918f13 commit 63d6c36

8 files changed

Lines changed: 53 additions & 39 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
558558
> [!CAUTION]
559559
> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
560560
561+
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.
562+
561563
## Network options
562564

563565
### Retries

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

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

stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/ModelConfig.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ private constructor(
197197
.toList()
198198
return when (bestMatches.size) {
199199
// This can happen if what we're deserializing is completely incompatible with all
200-
// the possible variants (e.g. deserializing from array).
200+
// the possible variants (e.g. deserializing from boolean).
201201
0 -> ModelConfig(_json = json)
202202
1 -> bestMatches.single()
203203
// If there's more than one match with the highest validity, then use the first

stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActParams.kt

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

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package com.browserbase.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
}

stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ModelConfigTest.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
99
import org.assertj.core.api.Assertions.assertThat
1010
import org.junit.jupiter.api.Test
1111
import org.junit.jupiter.api.assertThrows
12+
import org.junit.jupiter.params.ParameterizedTest
13+
import org.junit.jupiter.params.provider.EnumSource
1214

1315
internal class ModelConfigTest {
1416

@@ -74,10 +76,17 @@ internal class ModelConfigTest {
7476
assertThat(roundtrippedModelConfig).isEqualTo(modelConfig)
7577
}
7678

77-
@Test
78-
fun incompatibleJsonShapeDeserializesToUnknown() {
79-
val value = JsonValue.from(listOf("invalid", "array"))
80-
val modelConfig = jsonMapper().convertValue(value, jacksonTypeRef<ModelConfig>())
79+
enum class IncompatibleJsonShapeTestCase(val value: JsonValue) {
80+
BOOLEAN(JsonValue.from(false)),
81+
INTEGER(JsonValue.from(-1)),
82+
FLOAT(JsonValue.from(3.14)),
83+
ARRAY(JsonValue.from(listOf("invalid", "array"))),
84+
}
85+
86+
@ParameterizedTest
87+
@EnumSource
88+
fun incompatibleJsonShapeDeserializesToUnknown(testCase: IncompatibleJsonShapeTestCase) {
89+
val modelConfig = jsonMapper().convertValue(testCase.value, jacksonTypeRef<ModelConfig>())
8190

8291
val e = assertThrows<StagehandInvalidDataException> { modelConfig.validate() }
8392
assertThat(e).hasMessageStartingWith("Unknown ")

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