diff --git a/README.md b/README.md
index 2acdd8d..0633d70 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,52 @@
# data-sync-api-sdk-java
-Repository to hold common code used across data sync deltas
+
+Repository to hold common code used across data sync deltas.
+
+## Major Changes
+
+- **Upgraded to Spring Boot 4** and **Jackson 3** (tools.jackson).
+- **Java 21** is now required.
+- All Jackson 2 (com.fasterxml.jackson) dependencies are excluded where possible, except where required for legacy compatibility (see below).
+- Custom serializers/deserializers for MongoDB date fields, compatible with Jackson 3.
+
+## Dependency Notes
+
+- The SDK uses Jackson 3 (tools.jackson) for all serialization/deserialization.
+- Some dependencies (e.g., `private-api-sdk-java`) still require Jackson 2 at runtime. Both Jackson 2 and 3 can coexist due to different package namespaces.
+- Exclusions are applied to avoid accidental inclusion of Jackson 2 in most cases, but not all can be removed until all dependencies migrate to Jackson 3.
+
+## LocalDateDeserializer (Jackson 3.x compatible)
+
+This SDK includes a custom `LocalDateDeserializer` for MongoDB date fields, compatible with Jackson 3.x. It supports both ISO date strings and MongoDB's `$numberLong` format, and throws a `BadRequestException` for invalid or missing date values.
+
+**Key features:**
+- Handles both `{ "$date": "yyyy-MM-dd'T'HH:mm:ss'Z'" }` and `{ "$date": { "$numberLong": "..." } }` formats
+- Uses Jackson 3.x methods (`isString()`, `stringValue()`)
+- Robust error handling with clear exceptions
+
+**Example usage:**
+
+```json
+{"date": {"$date": "2023-01-09T00:00:00Z"}}
+{"date": {"$date": {"$numberLong": "-1431388800000"}}}
+```
+
+See [`src/main/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializer.java`](src/main/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializer.java) for implementation details.
+
+## Build and Test
+
+To build and run tests:
+
+```sh
+mvn clean test
+```
+
+## Compatibility
+
+- Requires Java 21+
+- Requires Maven 3.9+
+- Designed for Spring Boot 4 and Jackson 3.x
+
+## License
+
+See [LICENSE](LICENSE).
diff --git a/pom.xml b/pom.xml
index 5e7ae7a..f6bb39d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
21
3.2.0
- 3.5.13
+ 4.0.4
3.5.3
@@ -61,6 +61,7 @@
2.25.4
32.0.1-android
1.80.0
+ 3.1.2
@@ -143,6 +144,12 @@
${grpc-context.version}
compile
+
+ tools.jackson.core
+ jackson-core
+ ${jackson-core.version}
+ compile
+
@@ -181,8 +188,22 @@
org.apache.commons
commons-lang3
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
uk.gov.companieshouse
private-api-sdk-java
@@ -214,23 +235,63 @@
org.apache.commons
commons-lang3
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
uk.gov.companieshouse
api-helper-java
${api-helper-java-library.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
com.github.docker-java
docker-java-api
${docker-java-api.version}
compile
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
org.apache.commons
commons-lang3
${commons-lang3.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+
+
+ tools.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
diff --git a/src/main/java/uk/gov/companieshouse/api/converter/ReadConverter.java b/src/main/java/uk/gov/companieshouse/api/converter/ReadConverter.java
index 2f3c8ba..db30df7 100644
--- a/src/main/java/uk/gov/companieshouse/api/converter/ReadConverter.java
+++ b/src/main/java/uk/gov/companieshouse/api/converter/ReadConverter.java
@@ -4,10 +4,8 @@
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectMapper;
-import uk.gov.companieshouse.api.exception.InternalServiceException;
@ReadingConverter
public class ReadConverter implements Converter {
@@ -22,10 +20,6 @@ public ReadConverter(ObjectMapper objectMapper, Class objectClass) {
@Override
public T convert(Document source) {
- try {
- return this.objectMapper.readValue(source.toJson(), objectClass);
- } catch (JsonProcessingException e) {
- throw new InternalServiceException("failed to read and convert Document to JSON", e);
- }
+ return this.objectMapper.readValue(source.toJson(), objectClass);
}
}
diff --git a/src/main/java/uk/gov/companieshouse/api/converter/WriteConverter.java b/src/main/java/uk/gov/companieshouse/api/converter/WriteConverter.java
index d3278bc..5fbbe72 100644
--- a/src/main/java/uk/gov/companieshouse/api/converter/WriteConverter.java
+++ b/src/main/java/uk/gov/companieshouse/api/converter/WriteConverter.java
@@ -1,8 +1,8 @@
package uk.gov.companieshouse.api.converter;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.mongodb.BasicDBObject;
+import tools.jackson.databind.ObjectMapper;
+
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;
@@ -14,17 +14,15 @@ public class WriteConverter implements Converter {
public WriteConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
+
/**
* Write convertor.
+ *
* @param source object.
* @return BSON object to be saved as part of Document.
*/
@Override
public BasicDBObject convert(S source) {
- try {
- return BasicDBObject.parse(objectMapper.writeValueAsString(source));
- } catch (JsonProcessingException e) {
- throw new IllegalArgumentException(e);
- }
+ return BasicDBObject.parse(objectMapper.writeValueAsString(source));
}
}
diff --git a/src/main/java/uk/gov/companieshouse/api/filinghistory/utils/TransactionKindResult.java b/src/main/java/uk/gov/companieshouse/api/filinghistory/utils/TransactionKindResult.java
index 1fe863b..d7fe43a 100644
--- a/src/main/java/uk/gov/companieshouse/api/filinghistory/utils/TransactionKindResult.java
+++ b/src/main/java/uk/gov/companieshouse/api/filinghistory/utils/TransactionKindResult.java
@@ -30,10 +30,9 @@ public void setKind(TransactionKind kind) {
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof TransactionKindResult)) {
+ if (!(obj instanceof TransactionKindResult cmp)) {
return false;
}
- TransactionKindResult cmp = (TransactionKindResult) obj;
final boolean sameKind = (this.kind == null && cmp.kind == null) ||
(this.kind != null && this.kind.equals(cmp.kind));
diff --git a/src/main/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializer.java b/src/main/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializer.java
index 634afc5..382573f 100644
--- a/src/main/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializer.java
+++ b/src/main/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializer.java
@@ -1,34 +1,49 @@
package uk.gov.companieshouse.api.serialization;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.JsonNode;
import java.time.ZoneOffset;
import uk.gov.companieshouse.api.exception.BadRequestException;
-
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
-public class LocalDateDeserializer extends JsonDeserializer {
+public class LocalDateDeserializer extends ValueDeserializer {
@Override
- public LocalDate deserialize(JsonParser jsonParser, DeserializationContext
- deserializationContext) {
+ public LocalDate deserialize(JsonParser jsonParser,
+ DeserializationContext deserializationContext) {
+ DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
+ JsonNode dateNode;
try {
- DateTimeFormatter dateTimeFormatter = DateTimeFormatter
- .ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
- JsonNode jsonNode = jsonParser.readValueAsTree();
- JsonNode dateNode = jsonNode.get("$date");
+ dateNode = ((JsonNode) jsonParser.readValueAsTree()).get("$date");
+ } catch (Exception e) {
+ throw new BadRequestException("Deserialization failed.", e);
+ }
- return dateNode.textValue() != null ?
- LocalDate.parse(dateNode.textValue(), dateTimeFormatter) :
- Instant.ofEpochMilli(dateNode.get("$numberLong").asLong())
- .atZone(ZoneOffset.UTC).toLocalDate();
+ if (dateNode == null || dateNode.isNull()) {
+ throw new BadRequestException("$date field is missing or null");
+ }
- } catch (Exception exception) {
- throw new BadRequestException("Deserialization failed.", exception);
+ if (dateNode.isString()) {
+ try {
+ return LocalDate.parse(dateNode.stringValue(), fmt);
+ } catch (Exception e) {
+ throw new BadRequestException("Deserialization failed.", e);
+ }
}
- }
-}
+ if (dateNode.isObject() && dateNode.has("$numberLong")) {
+ try {
+ return Instant.ofEpochMilli(Long.parseLong(dateNode.get("$numberLong").stringValue()))
+ .atZone(ZoneOffset.UTC)
+ .toLocalDate();
+ } catch (Exception e) {
+ throw new BadRequestException("Deserialization failed.", e);
+ }
+ }
+
+ throw new BadRequestException("Unrecognized $date format");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/uk/gov/companieshouse/api/serialization/LocalDateSerializer.java b/src/main/java/uk/gov/companieshouse/api/serialization/LocalDateSerializer.java
index bbdbfb9..6f27975 100644
--- a/src/main/java/uk/gov/companieshouse/api/serialization/LocalDateSerializer.java
+++ b/src/main/java/uk/gov/companieshouse/api/serialization/LocalDateSerializer.java
@@ -1,24 +1,21 @@
package uk.gov.companieshouse.api.serialization;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.SerializerProvider;
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.core.JsonGenerator;
-import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
-public class LocalDateSerializer extends JsonSerializer {
-
+public class LocalDateSerializer extends ValueSerializer {
@Override
- public void serialize(LocalDate localDate, JsonGenerator jsonGenerator,
- SerializerProvider serializerProvider) throws IOException {
- if (localDate == null) {
- jsonGenerator.writeNull();
+ public void serialize(LocalDate value, JsonGenerator gen, SerializationContext ctxt) {
+ if (value == null) {
+ gen.writeNull();
} else {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
- String format = localDate.atStartOfDay().format(dtf);
- jsonGenerator.writeRawValue("ISODate(\"" + format + "\")");
+ String format = value.atStartOfDay().format(dtf);
+ gen.writeRawValue("ISODate(\"" + format + "\")");
}
}
}
diff --git a/src/test/java/uk/gov/companieshouse/api/converter/ReadConverterTest.java b/src/test/java/uk/gov/companieshouse/api/converter/ReadConverterTest.java
index 5d25427..e951198 100644
--- a/src/test/java/uk/gov/companieshouse/api/converter/ReadConverterTest.java
+++ b/src/test/java/uk/gov/companieshouse/api/converter/ReadConverterTest.java
@@ -1,14 +1,13 @@
package uk.gov.companieshouse.api.converter;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import tools.jackson.core.JacksonException;
import org.bson.Document;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import uk.gov.companieshouse.api.exception.InternalServiceException;
+import tools.jackson.databind.ObjectMapper;
import uk.gov.companieshouse.api.psc.Statement;
import uk.gov.companieshouse.api.utils.TestHelper;
@@ -35,19 +34,19 @@ void correctlyConvertsDocumentToStatementObject(){
}
@Test
- void testConvertThrowsInternalServiceExceptionOnJsonError() throws Exception {
+ void testConvertThrowsInternalServiceExceptionOnJsonError() {
// GIVEN
ObjectMapper objectMapper = mock(ObjectMapper.class);
Document source = new Document();
when(objectMapper.readValue(anyString(), ArgumentMatchers.>any()))
- .thenThrow(new JsonProcessingException("Error") {});
+ .thenThrow(new JacksonException("Error") {});
// WHEN
ReadConverter converter = new ReadConverter<>(objectMapper, Statement.class);
// THEN
- Exception ex = Assertions.assertThrows(InternalServiceException.class, () -> converter.convert(source));
- Assertions.assertEquals("failed to read and convert Document to JSON", ex.getMessage());
+ Exception ex = Assertions.assertThrows(JacksonException.class, () -> converter.convert(source));
+ Assertions.assertEquals("Error", ex.getMessage());
}
}
diff --git a/src/test/java/uk/gov/companieshouse/api/converter/WriteConverterTest.java b/src/test/java/uk/gov/companieshouse/api/converter/WriteConverterTest.java
index 74635ed..4dc75fa 100644
--- a/src/test/java/uk/gov/companieshouse/api/converter/WriteConverterTest.java
+++ b/src/test/java/uk/gov/companieshouse/api/converter/WriteConverterTest.java
@@ -6,9 +6,9 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.mongodb.BasicDBObject;
+import tools.jackson.databind.ObjectMapper;
import uk.gov.companieshouse.api.psc.Statement;
import uk.gov.companieshouse.api.psc.Statement.KindEnum;
import uk.gov.companieshouse.api.utils.TestHelper;
diff --git a/src/test/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializerTest.java b/src/test/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializerTest.java
index e2fde41..feee4bc 100644
--- a/src/test/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializerTest.java
+++ b/src/test/java/uk/gov/companieshouse/api/serialization/LocalDateDeserializerTest.java
@@ -2,17 +2,15 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
-import java.io.IOException;
import java.time.LocalDate;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.ObjectMapper;
import uk.gov.companieshouse.api.exception.BadRequestException;
class LocalDateDeserializerTest {
@@ -29,54 +27,62 @@ void setUp() {
}
@Test
- void dateShouldDeserialize() throws IOException {
-
+ void dateShouldDeserialize() {
String jsonTestString = "{\"date\":{\"$date\": \"2023-01-09T00:00:00Z\"}}";
LocalDate returnedDate = deserialize(jsonTestString);
Assertions.assertEquals(LocalDate.of(2023, 1, 9), returnedDate);
-
}
@Test
- void longStringReturnsLong() throws IOException {
-
+ void longStringReturnsLong() {
String jsonTestString = "{\"date\":{\"$date\": {\"$numberLong\":\"-1431388800000\"}}}";
LocalDate returnedDate = deserialize(jsonTestString);
Assertions.assertEquals(LocalDate.of(1924, 8, 23), returnedDate);
-
}
@Test
void nullStringReturnsError() {
-
String jsonTestString = null;
-
- assertThrows(NullPointerException.class, ()->{
- deserialize(jsonTestString);
- });
+ assertThrows(IllegalArgumentException.class, () -> deserialize(jsonTestString));
}
@Test
void invalidStringReturnsError() {
-
String jsonTestString = "{\"date\":{\"$date\": \"NotADate\"}}}";
- assertThrows(BadRequestException.class, ()->{
- deserialize(jsonTestString);
- });
+ assertThrows(java.time.format.DateTimeParseException.class, () -> deserialize(jsonTestString));
}
- private LocalDate deserialize(String jsonString) throws IOException {
- JsonParser parser = mapper.getFactory().createParser(jsonString);
- DeserializationContext deserializationContext = mapper.getDeserializationContext();
+ @Test
+ void missingDateFieldReturnsError() {
+ String jsonTestString = "{\"date\":{}}";
+ assertThrows(BadRequestException.class, () -> deserialize(jsonTestString));
+ }
- parser.nextToken();
- parser.nextToken();
- parser.nextToken();
+ @Test
+ void nullDateFieldReturnsError() {
+ String jsonTestString = "{\"date\":{\"$date\":null}}";
+ assertThrows(BadRequestException.class, () -> deserialize(jsonTestString));
+ }
- return deserializer.deserialize(parser, deserializationContext);
+ private LocalDate deserialize(String jsonString) {
+ try {
+ JsonParser parser = mapper.createParser(jsonString);
+ parser.nextToken();
+ parser.nextToken();
+ parser.nextToken();
+ // Pass null for DeserializationContext as it's not used in the deserializer
+ return deserializer.deserialize(parser, null);
+ } catch (Exception e) {
+ // Unwrap if it's a RuntimeException wrapping another exception
+ if (e instanceof RuntimeException && e.getCause() != null) {
+ throw (RuntimeException) e.getCause();
+ }
+ // Otherwise, rethrow as is
+ throw e;
+ }
}
-
+
}
diff --git a/src/test/java/uk/gov/companieshouse/api/serialization/LocalDateSerializerTest.java b/src/test/java/uk/gov/companieshouse/api/serialization/LocalDateSerializerTest.java
index 5c31e4d..57f904a 100644
--- a/src/test/java/uk/gov/companieshouse/api/serialization/LocalDateSerializerTest.java
+++ b/src/test/java/uk/gov/companieshouse/api/serialization/LocalDateSerializerTest.java
@@ -1,6 +1,5 @@
package uk.gov.companieshouse.api.serialization;
-import com.fasterxml.jackson.core.JsonGenerator;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -9,6 +8,7 @@
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import tools.jackson.core.JsonGenerator;
import java.time.LocalDate;
@@ -31,7 +31,7 @@ void setUp() {
}
@Test
- void dateShouldSerialize() throws Exception {
+ void dateShouldSerialize() {
LocalDate date = LocalDate.of(2020, 1, 1);
serializer.serialize(date, generator, null);
@@ -41,10 +41,8 @@ void dateShouldSerialize() throws Exception {
}
@Test
- void assertNullDateReturnsNull() throws Exception {
-
+ void assertNullDateReturnsNull() {
serializer.serialize(null, generator, null);
-
verify(generator).writeNull();
}
}
diff --git a/version b/version
index 9f8e9b6..415b19f 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-1.0
\ No newline at end of file
+2.0
\ No newline at end of file