Skip to content

Commit 6b09373

Browse files
authored
Merge pull request #849 from commercetools/date-deserializer
fix exception with invalid date deserialization
2 parents fd3cdf6 + 65d0484 commit 6b09373

File tree

7 files changed

+166
-18
lines changed

7 files changed

+166
-18
lines changed

commercetools/commercetools-sdk-java-api/src/main/java/com/commercetools/api/json/ApiModule.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11

22
package com.commercetools.api.json;
33

4+
import java.util.Map;
45
import java.util.Optional;
56

67
import com.commercetools.api.models.product.AttributeImpl;
78
import com.commercetools.api.models.product_search.ProductSearchFacetResult;
89
import com.commercetools.api.models.type.FieldContainerImpl;
10+
import com.fasterxml.jackson.core.type.TypeReference;
911
import com.fasterxml.jackson.databind.module.SimpleModule;
1012

1113
import io.vrap.rmf.base.client.utils.json.modules.ModuleOptions;
@@ -36,20 +38,30 @@ public ApiModule(ModuleOptions options) {
3638
Optional.ofNullable(options.getOption(ApiModuleOptions.DESERIALIZE_CUSTOM_FIELD_NUMBER_AS_DOUBLE))
3739
.orElse(System.getProperty(ApiModuleOptions.DESERIALIZE_CUSTOM_FIELD_NUMBER_AS_DOUBLE)));
3840

41+
final Map<String, TypeReference<?>> attributeTypes;
42+
final Map<String, TypeReference<?>> customFieldTypes;
43+
if (options instanceof ApiModuleOptions) {
44+
attributeTypes = ((ApiModuleOptions) options).getAttributeTypes();
45+
customFieldTypes = ((ApiModuleOptions) options).getCustomFieldTypes();
46+
}
47+
else {
48+
attributeTypes = null;
49+
customFieldTypes = null;
50+
}
3951
setMixInAnnotation(ProductSearchFacetResult.class, ProductSearchFacetResultMixin.class);
4052
if (attributeAsJsonNode) {
4153
setMixInAnnotation(AttributeImpl.class, AttributeJsonNodeMixin.class);
4254
}
4355
else {
4456
addDeserializer(AttributeImpl.class,
45-
new AttributeDeserializer(attributeAsDateString, attributeNumberAsDouble));
57+
new AttributeDeserializer(attributeAsDateString, attributeNumberAsDouble, attributeTypes));
4658
}
4759
if (customFieldAsJsonNode) {
4860
addDeserializer(FieldContainerImpl.class, new CustomFieldJsonNodeDeserializer());
4961
}
5062
else {
5163
addDeserializer(FieldContainerImpl.class,
52-
new CustomFieldDeserializer(customFieldAsDateString, customFieldNumberAsDouble));
64+
new CustomFieldDeserializer(customFieldAsDateString, customFieldNumberAsDouble, customFieldTypes));
5365
}
5466
}
5567
}

commercetools/commercetools-sdk-java-api/src/main/java/com/commercetools/api/json/ApiModuleOptions.java

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11

22
package com.commercetools.api.json;
33

4+
import java.util.Map;
5+
6+
import com.fasterxml.jackson.core.type.TypeReference;
7+
48
import io.vrap.rmf.base.client.utils.json.modules.ModuleOptions;
59

610
public class ApiModuleOptions implements ModuleOptions {
@@ -24,24 +28,32 @@ public class ApiModuleOptions implements ModuleOptions {
2428
private final Boolean attributeNumberAsDouble;
2529
private final Boolean customFieldNumberAsDouble;
2630

31+
private final Map<String, TypeReference<?>> attributeTypes;
32+
private final Map<String, TypeReference<?>> customFieldTypes;
33+
2734
private ApiModuleOptions() {
2835
this.dateAttributeAsString = false;
2936
this.dateCustomFieldAsString = false;
3037
this.attributeAsJsonNode = false;
3138
this.customFieldAsJsonNode = false;
3239
this.attributeNumberAsDouble = false;
3340
this.customFieldNumberAsDouble = false;
41+
this.attributeTypes = null;
42+
this.customFieldTypes = null;
3443
}
3544

3645
private ApiModuleOptions(final Boolean dateAttributeAsString, final Boolean dateCustomFieldAsString,
3746
final Boolean attributeAsJsonNode, final Boolean customFieldAsJsonNode,
38-
final Boolean attributeNumberAsDouble, final Boolean customFieldNumberAsDouble) {
47+
final Boolean attributeNumberAsDouble, final Boolean customFieldNumberAsDouble,
48+
final Map<String, TypeReference<?>> attributeTypes, final Map<String, TypeReference<?>> customFieldTypes) {
3949
this.dateAttributeAsString = dateAttributeAsString;
4050
this.dateCustomFieldAsString = dateCustomFieldAsString;
4151
this.attributeAsJsonNode = attributeAsJsonNode;
4252
this.customFieldAsJsonNode = customFieldAsJsonNode;
4353
this.attributeNumberAsDouble = attributeNumberAsDouble;
4454
this.customFieldNumberAsDouble = customFieldNumberAsDouble;
55+
this.attributeTypes = attributeTypes;
56+
this.customFieldTypes = customFieldTypes;
4557
}
4658

4759
public static ApiModuleOptions of() {
@@ -72,34 +84,60 @@ public Boolean getCustomFieldNumberAsDouble() {
7284
return customFieldNumberAsDouble;
7385
}
7486

87+
public Map<String, TypeReference<?>> getAttributeTypes() {
88+
return attributeTypes;
89+
}
90+
91+
public Map<String, TypeReference<?>> getCustomFieldTypes() {
92+
return customFieldTypes;
93+
}
94+
7595
public ApiModuleOptions withDateCustomFieldAsString(Boolean dateCustomFieldAsString) {
7696
return new ApiModuleOptions(dateAttributeAsString, dateCustomFieldAsString, attributeAsJsonNode,
77-
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble);
97+
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble, attributeTypes,
98+
customFieldTypes);
7899
}
79100

80101
public ApiModuleOptions withDateAttributeAsString(Boolean dateAttributeAsString) {
81102
return new ApiModuleOptions(dateAttributeAsString, dateCustomFieldAsString, attributeAsJsonNode,
82-
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble);
103+
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble, attributeTypes,
104+
customFieldTypes);
83105
}
84106

85107
public ApiModuleOptions withAttributeAsJsonNode(Boolean attributeAsJsonNode) {
86108
return new ApiModuleOptions(dateAttributeAsString, dateCustomFieldAsString, attributeAsJsonNode,
87-
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble);
109+
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble, attributeTypes,
110+
customFieldTypes);
88111
}
89112

90113
public ApiModuleOptions withCustomFieldAsJsonNode(Boolean customFieldAsJsonNode) {
91114
return new ApiModuleOptions(dateAttributeAsString, dateCustomFieldAsString, attributeAsJsonNode,
92-
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble);
115+
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble, attributeTypes,
116+
customFieldTypes);
93117
}
94118

95119
public ApiModuleOptions withCustomFieldNumberAsDouble(Boolean customFieldNumberAsDouble) {
96120
return new ApiModuleOptions(dateAttributeAsString, dateCustomFieldAsString, attributeAsJsonNode,
97-
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble);
121+
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble, attributeTypes,
122+
customFieldTypes);
98123
}
99124

100125
public ApiModuleOptions withAttributeNumberAsDouble(Boolean attributeNumberAsDouble) {
101126
return new ApiModuleOptions(dateAttributeAsString, dateCustomFieldAsString, attributeAsJsonNode,
102-
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble);
127+
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble, attributeTypes,
128+
customFieldTypes);
129+
}
130+
131+
public ApiModuleOptions withCustomFieldTypes(Map<String, TypeReference<?>> customFieldTypes) {
132+
return new ApiModuleOptions(dateAttributeAsString, dateCustomFieldAsString, attributeAsJsonNode,
133+
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble, attributeTypes,
134+
customFieldTypes);
135+
}
136+
137+
public ApiModuleOptions withAttributeTypes(Map<String, TypeReference<?>> attributeTypes) {
138+
return new ApiModuleOptions(dateAttributeAsString, dateCustomFieldAsString, attributeAsJsonNode,
139+
customFieldAsJsonNode, attributeNumberAsDouble, customFieldNumberAsDouble, attributeTypes,
140+
customFieldTypes);
103141
}
104142

105143
@Override

commercetools/commercetools-sdk-java-api/src/main/java/com/commercetools/api/json/AttributeDeserializer.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.time.LocalTime;
77
import java.time.ZonedDateTime;
88
import java.util.List;
9+
import java.util.Map;
910
import java.util.regex.Pattern;
1011

1112
import com.commercetools.api.models.common.LocalizedString;
@@ -24,28 +25,40 @@
2425
public class AttributeDeserializer extends JsonDeserializer<AttributeImpl> {
2526

2627
private static Pattern p = Pattern.compile("^[0-9]");
27-
private static Pattern dateTime = Pattern
28-
.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})");
29-
private static Pattern date = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}");
28+
private static Pattern dateTime = Pattern.compile(
29+
"^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})");
30+
private static Pattern date = Pattern.compile("^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
3031
private static Pattern time = Pattern.compile("^[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]{1,9})?");
3132

3233
private final boolean deserializeAsDate;
3334

3435
private final boolean deserializeNumberAsDouble;
3536

37+
private final Map<String, TypeReference<?>> attributeTypes;
38+
39+
public AttributeDeserializer(boolean deserializeAsDateString, boolean deserializeNumberAsDouble,
40+
final Map<String, TypeReference<?>> attributeTypes) {
41+
this.deserializeAsDate = !deserializeAsDateString;
42+
this.deserializeNumberAsDouble = deserializeNumberAsDouble;
43+
this.attributeTypes = attributeTypes;
44+
}
45+
3646
public AttributeDeserializer(boolean deserializeAsDateString) {
3747
this.deserializeAsDate = !deserializeAsDateString;
3848
this.deserializeNumberAsDouble = false;
49+
this.attributeTypes = null;
3950
}
4051

4152
public AttributeDeserializer(boolean deserializeAsDateString, boolean deserializeNumberAsDouble) {
4253
this.deserializeAsDate = !deserializeAsDateString;
4354
this.deserializeNumberAsDouble = deserializeNumberAsDouble;
55+
this.attributeTypes = null;
4456
}
4557

4658
public AttributeDeserializer() {
4759
this.deserializeAsDate = true;
4860
this.deserializeNumberAsDouble = false;
61+
this.attributeTypes = null;
4962
}
5063

5164
@Override
@@ -54,9 +67,15 @@ public AttributeImpl deserialize(JsonParser p, DeserializationContext ctx) throw
5467
JsonNode node = p.readValueAsTree();
5568
JsonNode valueNode = node.get("value");
5669

70+
String name = node.get("name").asText();
5771
AttributeBuilder builder = Attribute.builder();
58-
builder.name(node.get("name").asText());
72+
builder.name(name);
5973

74+
if (attributeTypes != null && attributeTypes.containsKey(name)) {
75+
return (AttributeImpl) builder
76+
.value(p.getCodec().treeAsTokens(valueNode).readValueAs(attributeTypes.get(name)))
77+
.build();
78+
}
6079
return (AttributeImpl) builder.value(p.getCodec().treeAsTokens(valueNode).readValueAs(typeRef(valueNode)))
6180
.build();
6281
}

commercetools/commercetools-sdk-java-api/src/main/java/com/commercetools/api/json/CustomFieldDeserializer.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.time.LocalTime;
77
import java.time.ZonedDateTime;
88
import java.util.List;
9+
import java.util.Map;
910
import java.util.regex.Pattern;
1011

1112
import com.commercetools.api.models.common.LocalizedString;
@@ -22,27 +23,39 @@
2223
public class CustomFieldDeserializer extends JsonDeserializer<FieldContainerImpl> {
2324

2425
private static Pattern p = Pattern.compile("^[0-9]");
25-
private static Pattern dateTime = Pattern
26-
.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})");
27-
private static Pattern date = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}");
26+
private static Pattern dateTime = Pattern.compile(
27+
"^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})");
28+
private static Pattern date = Pattern.compile("^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
2829
private static Pattern time = Pattern.compile("^[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]{1,9})?");
2930

3031
private final boolean deserializeAsDate;
3132
private final boolean deserializeNumberAsDouble;
3233

34+
private final Map<String, TypeReference<?>> customFieldTypes;
35+
36+
public CustomFieldDeserializer(boolean deserializeAsDateString, boolean deserializeNumberAsDouble,
37+
final Map<String, TypeReference<?>> customFieldTypes) {
38+
this.deserializeAsDate = !deserializeAsDateString;
39+
this.deserializeNumberAsDouble = deserializeNumberAsDouble;
40+
this.customFieldTypes = customFieldTypes;
41+
}
42+
3343
public CustomFieldDeserializer(boolean deserializeAsDateString) {
3444
this.deserializeAsDate = !deserializeAsDateString;
3545
this.deserializeNumberAsDouble = false;
46+
this.customFieldTypes = null;
3647
}
3748

3849
public CustomFieldDeserializer(boolean deserializeAsDateString, boolean deserializeNumberAsDouble) {
3950
this.deserializeAsDate = !deserializeAsDateString;
4051
this.deserializeNumberAsDouble = deserializeNumberAsDouble;
52+
this.customFieldTypes = null;
4153
}
4254

4355
public CustomFieldDeserializer() {
4456
this.deserializeAsDate = true;
4557
this.deserializeNumberAsDouble = false;
58+
this.customFieldTypes = null;
4659
}
4760

4861
@Override
@@ -53,13 +66,17 @@ public FieldContainerImpl deserialize(JsonParser p, DeserializationContext ctx)
5366
FieldContainerBuilder builder = FieldContainerBuilder.of();
5467

5568
node.fields()
56-
.forEachRemaining(nodeEntry -> builder.addValue(nodeEntry.getKey(), mapValue(p, nodeEntry.getValue())));
69+
.forEachRemaining(nodeEntry -> builder.addValue(nodeEntry.getKey(),
70+
mapValue(p, nodeEntry.getKey(), nodeEntry.getValue())));
5771

5872
return (FieldContainerImpl) builder.build();
5973
}
6074

61-
private Object mapValue(JsonParser p, JsonNode nodeValue) {
75+
private Object mapValue(final JsonParser p, final String name, final JsonNode nodeValue) {
6276
try {
77+
if (customFieldTypes != null && customFieldTypes.containsKey(name)) {
78+
return p.getCodec().treeAsTokens(nodeValue).readValueAs(customFieldTypes.get(name));
79+
}
6380
return p.getCodec().treeAsTokens(nodeValue).readValueAs(typeRef(nodeValue));
6481
}
6582
catch (IOException e) {

commercetools/commercetools-sdk-java-api/src/test/java/com/commercetools/AttributesTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
import com.commercetools.api.models.product_type.AttributePlainEnumValue;
2020
import com.commercetools.api.models.product_type.AttributePlainEnumValueBuilder;
2121
import com.fasterxml.jackson.core.JsonProcessingException;
22+
import com.fasterxml.jackson.core.type.TypeReference;
2223
import com.fasterxml.jackson.databind.JsonNode;
2324
import com.fasterxml.jackson.databind.ObjectMapper;
2425

2526
import io.vrap.rmf.base.client.utils.json.JsonUtils;
2627

2728
import org.assertj.core.api.Assertions;
2829
import org.assertj.core.util.Lists;
30+
import org.assertj.core.util.Maps;
2931
import org.junit.jupiter.api.Test;
3032

3133
public class AttributesTest {
@@ -189,6 +191,29 @@ public void attributesNumberAsDouble() throws JsonProcessingException {
189191
assertThat(variant.getAttribute("integer").getValue()).isEqualTo(10.0);
190192
}
191193

194+
@Test
195+
public void attributeTypeByName() throws JsonProcessingException {
196+
ApiModuleOptions options = ApiModuleOptions.of()
197+
.withAttributeTypes(Maps.newHashMap("test", new TypeReference<String>() {
198+
}));
199+
ObjectMapper mapper = JsonUtils.createObjectMapper(options);
200+
201+
Attribute attribute = mapper.readValue("{\"name\":\"test\", \"value\": \"2025-01-01\"}", Attribute.class);
202+
203+
assertThat(attribute.getValue()).isEqualTo("2025-01-01");
204+
205+
Attribute attributeDate = mapper.readValue("{\"name\":\"date\", \"value\": \"2025-01-01\"}", Attribute.class);
206+
assertThat(attributeDate.getValue()).isEqualTo(LocalDate.parse("2025-01-01"));
207+
208+
Attribute attributeInvalidMonth = mapper.readValue("{\"name\":\"invalid\", \"value\": \"2025-13-01\"}",
209+
Attribute.class);
210+
assertThat(attributeInvalidMonth.getValue()).isEqualTo("2025-13-01");
211+
212+
Attribute attributeInvalidDay = mapper.readValue("{\"name\":\"invalid\", \"value\": \"2025-12-32\"}",
213+
Attribute.class);
214+
assertThat(attributeInvalidDay.getValue()).isEqualTo("2025-12-32");
215+
}
216+
192217
@Test
193218
public void attributesAsDateFalse() throws IOException {
194219
ApiModuleOptions options = ApiModuleOptions.of()

commercetools/commercetools-sdk-java-api/src/test/java/com/commercetools/CustomFieldsTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
import com.commercetools.api.models.product.ProductReference;
1919
import com.commercetools.api.models.type.*;
2020
import com.fasterxml.jackson.core.JsonProcessingException;
21+
import com.fasterxml.jackson.core.type.TypeReference;
2122
import com.fasterxml.jackson.databind.JsonNode;
2223
import com.fasterxml.jackson.databind.ObjectMapper;
2324

2425
import io.vrap.rmf.base.client.utils.json.JsonUtils;
2526

2627
import org.assertj.core.api.Assertions;
2728
import org.assertj.core.util.Lists;
29+
import org.assertj.core.util.Maps;
2830
import org.junit.jupiter.api.Test;
2931

3032
public class CustomFieldsTest {
@@ -329,6 +331,33 @@ public void httpDeSerialize() throws IOException {
329331
Assertions.assertThat(serializedNumberAttributes).isEqualTo("{\"double\":13.0,\"decimal\":13.1,\"int\":13}");
330332
}
331333

334+
@Test
335+
public void customFieldTypeByName() throws JsonProcessingException {
336+
ApiModuleOptions options = ApiModuleOptions.of()
337+
.withCustomFieldTypes(Maps.newHashMap("test", new TypeReference<String>() {
338+
}));
339+
ObjectMapper mapper = JsonUtils.createObjectMapper(options);
340+
341+
CustomFields customFields = mapper.readValue(stringFromResource("customfields-dates.json"), CustomFields.class);
342+
343+
assertThat(customFields.getFields().values()).isNotEmpty();
344+
345+
CustomFieldsAccessor fields = customFields.withCustomFields(CustomFieldsAccessor::new);
346+
347+
assertThat(fields.get("test")).isInstanceOf(String.class);
348+
assertThat(fields.asString("test")).isEqualTo("2025-01-01");
349+
assertThat(fields.asDate("test")).isEqualTo(LocalDate.of(2025, 1, 1));
350+
351+
assertThat(fields.get("date")).isInstanceOf(LocalDate.class);
352+
assertThat(fields.asDate("date")).isEqualTo(LocalDate.of(2025, 1, 1));
353+
354+
assertThat(fields.get("invalidMonth")).isInstanceOf(String.class);
355+
assertThat(fields.asString("invalidMonth")).isEqualTo("2025-13-01");
356+
357+
assertThat(fields.get("invalidDay")).isInstanceOf(String.class);
358+
assertThat(fields.asString("invalidDay")).isEqualTo("2025-12-32");
359+
}
360+
332361
@Test
333362
public void serializeCustomFields() throws JsonProcessingException {
334363
FieldContainer container = FieldContainerBuilder.of()

0 commit comments

Comments
 (0)