Skip to content

Commit 3239a08

Browse files
committed
Fix string-to-long coercion when afterburner is enabled
Reported upstream: FasterXML/jackson-modules-base#120 The CoercionConfig is fantastic, unfortunately it is not yet supported by the AfterBurner module. Separately we may consider moving to the new Blackbird module on java 11 runtimes. For now I've updated our existing long deserializers for 2.12.0 which has the benefit of rejecting coercion from string to SafeLong.
1 parent 946bd0a commit 3239a08

File tree

5 files changed

+202
-16
lines changed

5 files changed

+202
-16
lines changed

conjure-java-client-verifier/src/test/resources/ignored-test-cases.jersey.yml

-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,6 @@ client:
9999
- 'null'
100100
receiveMapEnumExampleAlias:
101101
- 'null'
102-
receiveSafeLongExample: #allowed long coercion applies to safelong
103-
- '{"value":"12"}'
104102

105103
singleHeaderService: {}
106104

conjure-java-client-verifier/src/test/resources/ignored-test-cases.retrofit.yml

-2
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,6 @@ client:
136136
- 'null'
137137
receiveMapEnumExampleAlias:
138138
- 'null'
139-
receiveSafeLongExample: #allowed long coercion applies to safelong
140-
- '{"value":"12"}'
141139

142140
singleHeaderService: {}
143141

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.conjure.java.serialization;
18+
19+
import com.fasterxml.jackson.core.JsonParser;
20+
import com.fasterxml.jackson.databind.DeserializationContext;
21+
import com.fasterxml.jackson.databind.cfg.CoercionAction;
22+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
23+
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
24+
import com.fasterxml.jackson.databind.module.SimpleModule;
25+
import com.fasterxml.jackson.datatype.jdk8.OptionalLongDeserializer;
26+
import com.palantir.logsafe.exceptions.SafeIoException;
27+
import java.io.IOException;
28+
import java.util.OptionalLong;
29+
30+
/**
31+
* Provides support for the {@link Long} deserialization from JSON string and numeric values regardless of
32+
*
33+
* <pre>MapperFeature.ALLOW_COERCION_OF_SCALARS</pre>
34+
*
35+
* configuration.
36+
*/
37+
final class LenientLongModule extends SimpleModule {
38+
39+
LenientLongModule() {
40+
super("lenient long");
41+
// Register to both Long.TYPE and Long.class
42+
this.addDeserializer(long.class, new LongAsStringDeserializer())
43+
.addDeserializer(Long.class, new LongAsStringDeserializer())
44+
.addDeserializer(OptionalLong.class, new OptionalLongAsStringDeserializer());
45+
}
46+
47+
private static final class LongAsStringDeserializer extends StdDeserializer<Long> {
48+
49+
private LongAsStringDeserializer() {
50+
super(Long.TYPE);
51+
}
52+
53+
@Override
54+
public Long deserialize(JsonParser jsonParser, DeserializationContext _ctxt) throws IOException {
55+
switch (jsonParser.currentToken()) {
56+
case VALUE_NUMBER_INT:
57+
return jsonParser.getLongValue();
58+
case VALUE_STRING:
59+
return parseLong(jsonParser);
60+
case VALUE_NULL:
61+
return null;
62+
default:
63+
throw new SafeIoException("Expected a long value");
64+
}
65+
}
66+
67+
@Override
68+
public boolean isCachable() {
69+
return true;
70+
}
71+
72+
private static Long parseLong(JsonParser jsonParser) throws IOException {
73+
String value = jsonParser.getValueAsString();
74+
try {
75+
return Long.valueOf(value);
76+
} catch (NumberFormatException e) {
77+
InvalidFormatException failure =
78+
new InvalidFormatException(jsonParser, "not a valid long value", value, long.class);
79+
failure.initCause(e);
80+
throw failure;
81+
}
82+
}
83+
}
84+
85+
private static final class OptionalLongAsStringDeserializer extends OptionalLongDeserializer {
86+
87+
private OptionalLongAsStringDeserializer() {}
88+
89+
@Override
90+
protected CoercionAction _checkFromStringCoercion(DeserializationContext _ctxt, String _value) {
91+
return CoercionAction.TryConvert;
92+
}
93+
}
94+
}

conjure-java-jackson-serialization/src/main/java/com/palantir/conjure/java/serialization/ObjectMappers.java

+1-12
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,12 @@
2020
import com.fasterxml.jackson.databind.MapperFeature;
2121
import com.fasterxml.jackson.databind.ObjectMapper;
2222
import com.fasterxml.jackson.databind.SerializationFeature;
23-
import com.fasterxml.jackson.databind.cfg.CoercionAction;
24-
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
2523
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
2624
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
2725
import com.fasterxml.jackson.datatype.guava.GuavaModule;
2826
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
2927
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
3028
import com.fasterxml.jackson.module.afterburner.AfterburnerModule;
31-
import java.util.OptionalLong;
3229

3330
public final class ObjectMappers {
3431

@@ -131,14 +128,12 @@ public static ObjectMapper newSmileServerObjectMapper() {
131128
* </ul>
132129
*/
133130
public static ObjectMapper withDefaultModules(ObjectMapper mapper) {
134-
allowStringCoercion(mapper, long.class);
135-
allowStringCoercion(mapper, Long.class);
136-
allowStringCoercion(mapper, OptionalLong.class);
137131
return mapper.registerModule(new GuavaModule())
138132
.registerModule(new ShimJdk7Module())
139133
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
140134
.registerModule(new AfterburnerModule())
141135
.registerModule(new JavaTimeModule())
136+
.registerModule(new LenientLongModule())
142137
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
143138
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
144139
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
@@ -148,10 +143,4 @@ public static ObjectMapper withDefaultModules(ObjectMapper mapper) {
148143
.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
149144
.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT);
150145
}
151-
152-
private static void allowStringCoercion(ObjectMapper mapper, Class<?> clazz) {
153-
mapper.coercionConfigFor(clazz)
154-
.setAcceptBlankAsEmpty(false)
155-
.setCoercion(CoercionInputShape.String, CoercionAction.TryConvert);
156-
}
157146
}

conjure-java-jackson-serialization/src/test/java/com/palantir/conjure/java/serialization/ObjectMappersTest.java

+107
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2121

22+
import com.fasterxml.jackson.annotation.JsonProperty;
2223
import com.fasterxml.jackson.core.JsonParseException;
2324
import com.fasterxml.jackson.core.JsonProcessingException;
2425
import com.fasterxml.jackson.core.exc.InputCoercionException;
2526
import com.fasterxml.jackson.core.type.TypeReference;
2627
import com.fasterxml.jackson.databind.ObjectMapper;
2728
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
29+
import com.palantir.logsafe.Preconditions;
2830
import java.io.File;
2931
import java.io.IOException;
3032
import java.math.BigInteger;
@@ -38,6 +40,7 @@
3840
import java.time.ZonedDateTime;
3941
import java.util.Collections;
4042
import java.util.Map;
43+
import java.util.Objects;
4144
import java.util.Optional;
4245
import java.util.OptionalInt;
4346
import java.util.OptionalLong;
@@ -128,11 +131,115 @@ public void testLongTypeDeserializationFromString() throws IOException {
128131
assertThat(MAPPER.readValue("\"1\"", Long.TYPE)).isEqualTo(1L);
129132
}
130133

134+
@Test
135+
public void testLongBeanTypeDeserializationFromString() throws IOException {
136+
assertThat(MAPPER.readValue("{\"value\":\"1\"}", LongBean.class)).isEqualTo(new LongBean(1L));
137+
}
138+
139+
@Test
140+
public void testLongBeanTypeDeserializationFromNumber() throws IOException {
141+
assertThat(MAPPER.readValue("{\"value\":\"1\"}", LongBean.class)).isEqualTo(new LongBean(1L));
142+
}
143+
144+
static final class LongBean {
145+
@JsonProperty
146+
private long value;
147+
148+
LongBean() {}
149+
150+
LongBean(long value) {
151+
setValue(value);
152+
}
153+
154+
public long getValue() {
155+
return value;
156+
}
157+
158+
public void setValue(long value) {
159+
this.value = value;
160+
}
161+
162+
@Override
163+
public boolean equals(Object other) {
164+
if (this == other) {
165+
return true;
166+
}
167+
if (other == null || getClass() != other.getClass()) {
168+
return false;
169+
}
170+
LongBean that = (LongBean) other;
171+
return value == that.value;
172+
}
173+
174+
@Override
175+
public int hashCode() {
176+
return Objects.hashCode(value);
177+
}
178+
179+
@Override
180+
public String toString() {
181+
return "LongBean{value=" + value + '}';
182+
}
183+
}
184+
131185
@Test
132186
public void testOptionalLongTypeDeserializationFromString() throws IOException {
133187
assertThat(MAPPER.readValue("\"1\"", OptionalLong.class)).hasValue(1L);
134188
}
135189

190+
@Test
191+
public void testOptionalLongBeanTypeDeserializationFromString() throws IOException {
192+
assertThat(MAPPER.readValue("{\"value\":\"1\"}", OptionalLongBean.class))
193+
.isEqualTo(new OptionalLongBean(OptionalLong.of(1L)));
194+
}
195+
196+
@Test
197+
public void testOptionalLongBeanTypeDeserializationFromNumber() throws IOException {
198+
assertThat(MAPPER.readValue("{\"value\":1}", OptionalLongBean.class))
199+
.isEqualTo(new OptionalLongBean(OptionalLong.of(1L)));
200+
}
201+
202+
static final class OptionalLongBean {
203+
@JsonProperty
204+
private OptionalLong value = OptionalLong.empty();
205+
206+
OptionalLongBean() {}
207+
208+
OptionalLongBean(OptionalLong value) {
209+
setValue(value);
210+
}
211+
212+
public OptionalLong getValue() {
213+
return value;
214+
}
215+
216+
public void setValue(OptionalLong value) {
217+
this.value = Preconditions.checkNotNull(value, "value");
218+
}
219+
220+
@Override
221+
public boolean equals(Object other) {
222+
if (this == other) {
223+
return true;
224+
}
225+
if (other == null || getClass() != other.getClass()) {
226+
return false;
227+
}
228+
OptionalLongBean that = (OptionalLongBean) other;
229+
return value.equals(that.value);
230+
}
231+
232+
@Override
233+
public int hashCode() {
234+
return Objects.hashCode(value);
235+
}
236+
237+
@Override
238+
public String toString() {
239+
return "OptionalLongBean{value=" + value + '}';
240+
}
241+
}
242+
136243
@Test
137244
public void testLongDeserializationFromJsonNumber() throws IOException {
138245
assertThat(MAPPER.readValue("1", Long.class)).isEqualTo(1L);

0 commit comments

Comments
 (0)