Skip to content

Commit 15fddc3

Browse files
fix(client): limit json deserialization coercion (#102)
1 parent 5ce9c06 commit 15fddc3

File tree

4 files changed

+139
-97
lines changed

4 files changed

+139
-97
lines changed

openlayer-java-core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies {
3434
testImplementation("org.assertj:assertj-core:3.25.3")
3535
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
3636
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
37+
testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
3738
testImplementation("org.mockito:mockito-core:5.14.2")
3839
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
3940
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import com.fasterxml.jackson.databind.DeserializationFeature
88
import com.fasterxml.jackson.databind.MapperFeature
99
import com.fasterxml.jackson.databind.SerializationFeature
1010
import com.fasterxml.jackson.databind.SerializerProvider
11+
import com.fasterxml.jackson.databind.cfg.CoercionAction
12+
import com.fasterxml.jackson.databind.cfg.CoercionInputShape
1113
import com.fasterxml.jackson.databind.json.JsonMapper
1214
import com.fasterxml.jackson.databind.module.SimpleModule
15+
import com.fasterxml.jackson.databind.type.LogicalType
1316
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
1417
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
1518
import com.fasterxml.jackson.module.kotlin.kotlinModule
@@ -21,6 +24,60 @@ fun jsonMapper(): JsonMapper =
2124
.addModule(Jdk8Module())
2225
.addModule(JavaTimeModule())
2326
.addModule(SimpleModule().addSerializer(InputStreamJsonSerializer))
27+
.withCoercionConfig(LogicalType.Boolean) {
28+
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
29+
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
30+
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
31+
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
32+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
33+
}
34+
.withCoercionConfig(LogicalType.Integer) {
35+
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
36+
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
37+
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
38+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
39+
}
40+
.withCoercionConfig(LogicalType.Float) {
41+
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
42+
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
43+
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
44+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
45+
}
46+
.withCoercionConfig(LogicalType.Textual) {
47+
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
48+
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
49+
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
50+
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
51+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
52+
}
53+
.withCoercionConfig(LogicalType.Array) {
54+
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
55+
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
56+
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
57+
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
58+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
59+
}
60+
.withCoercionConfig(LogicalType.Collection) {
61+
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
62+
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
63+
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
64+
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
65+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
66+
}
67+
.withCoercionConfig(LogicalType.Map) {
68+
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
69+
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
70+
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
71+
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
72+
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
73+
}
74+
.withCoercionConfig(LogicalType.POJO) {
75+
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
76+
.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
77+
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
78+
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
79+
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
80+
}
2481
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
2582
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
2683
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.openlayer.api.core
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty
4+
import com.fasterxml.jackson.databind.exc.MismatchedInputException
5+
import kotlin.reflect.KClass
6+
import org.assertj.core.api.Assertions.assertThat
7+
import org.assertj.core.api.Assertions.catchThrowable
8+
import org.junit.jupiter.api.Test
9+
import org.junitpioneer.jupiter.cartesian.CartesianTest
10+
11+
internal class ObjectMappersTest {
12+
13+
internal class ClassWithBooleanFieldPrefixedWithIs(private val isActive: JsonField<Boolean>) {
14+
15+
@JsonProperty("is_active") @ExcludeMissing fun _isActive() = isActive
16+
}
17+
18+
@Test
19+
fun write_whenFieldPrefixedWithIs_keepsPrefix() {
20+
val value = ClassWithBooleanFieldPrefixedWithIs(JsonField.of(true))
21+
22+
val json = jsonMapper().writeValueAsString(value)
23+
24+
assertThat(json).isEqualTo("{\"is_active\":true}")
25+
}
26+
27+
internal class Class(@get:JsonProperty("field") @JsonProperty("field") val field: String)
28+
29+
enum class ShapeTestCase(val value: Any, val kClass: KClass<*>) {
30+
STRING("Hello World!", String::class),
31+
BOOLEAN(true, Boolean::class),
32+
FLOAT(3.14F, Float::class),
33+
DOUBLE(3.14, Double::class),
34+
INTEGER(42, Int::class),
35+
LONG(42L, Long::class),
36+
MAP(mapOf("property" to "value"), Map::class),
37+
CLASS(Class("Hello World!"), Class::class),
38+
LIST(listOf(1, 2, 3), List::class);
39+
40+
companion object {
41+
val VALID_CONVERSIONS =
42+
listOf(
43+
FLOAT to DOUBLE,
44+
FLOAT to INTEGER,
45+
FLOAT to LONG,
46+
DOUBLE to FLOAT,
47+
DOUBLE to INTEGER,
48+
DOUBLE to LONG,
49+
INTEGER to FLOAT,
50+
INTEGER to DOUBLE,
51+
INTEGER to LONG,
52+
LONG to FLOAT,
53+
LONG to DOUBLE,
54+
LONG to INTEGER,
55+
CLASS to MAP,
56+
// These aren't actually valid, but coercion configs don't work for String until
57+
// v2.14.0: https://github.com/FasterXML/jackson-databind/issues/3240
58+
// We currently test on v2.13.4.
59+
BOOLEAN to STRING,
60+
FLOAT to STRING,
61+
DOUBLE to STRING,
62+
INTEGER to STRING,
63+
LONG to STRING,
64+
)
65+
}
66+
}
67+
68+
@CartesianTest
69+
fun read(@CartesianTest.Enum shape1: ShapeTestCase, @CartesianTest.Enum shape2: ShapeTestCase) {
70+
val jsonMapper = jsonMapper()
71+
val json = jsonMapper.writeValueAsString(shape1.value)
72+
73+
val e = catchThrowable { jsonMapper.readValue(json, shape2.kClass.java) }
74+
75+
if (shape1 == shape2 || shape1 to shape2 in ShapeTestCase.VALID_CONVERSIONS) {
76+
assertThat(e).isNull()
77+
} else {
78+
assertThat(e).isInstanceOf(MismatchedInputException::class.java)
79+
}
80+
}
81+
}

openlayer-java-core/src/test/kotlin/com/openlayer/api/core/http/SerializerTest.kt

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)