Browse Source

Jackson 3.x GeoJson (de-)serializers.

Adds a GeoJsonJackson3Module.

Closes #5100
issue/5100-geojson
Jens Schauder 3 weeks ago
parent
commit
dea6701553
No known key found for this signature in database
GPG Key ID: 2BE5D185CD2A1CE6
  1. 6
      spring-data-mongodb/pom.xml
  2. 33
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GeoJsonJackson3Configuration.java
  3. 296
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonJackson3Module.java
  4. 1
      spring-data-mongodb/src/main/resources/META-INF/spring.factories
  5. 13
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/GeoJsonJackson3ConfigurationIntegrationTests.java
  6. 109
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonJackson3ModuleUnitTests.java
  7. 18
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonModuleUnitTests.java

6
spring-data-mongodb/pom.xml

@ -234,6 +234,12 @@ @@ -234,6 +234,12 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>

33
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GeoJsonJackson3Configuration.java

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.core.geo.GeoJsonJackson3Module;
import org.springframework.data.web.config.SpringDataJackson3Modules;
/**
* Configuration class to expose {@link GeoJsonJackson3Module} as a Spring bean.
*
* @author Jens Schauder
*/
public class GeoJsonJackson3Configuration implements SpringDataJackson3Modules {
@Bean
public GeoJsonJackson3Module geoJsonModule() {
return new GeoJsonJackson3Module();
}
}

296
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonJackson3Module.java

@ -0,0 +1,296 @@ @@ -0,0 +1,296 @@
package org.springframework.data.mongodb.core.geo;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.JsonParser;
import tools.jackson.core.Version;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.ValueSerializer;
import tools.jackson.databind.module.SimpleDeserializers;
import tools.jackson.databind.module.SimpleSerializers;
import tools.jackson.databind.node.ArrayNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Point;
import org.springframework.util.Assert;
public class GeoJsonJackson3Module {
private static Version version = new Version(3, 2, 0, null, "org.springframework.data",
"spring-data-mongodb-geojson");
public static class Serializers extends JacksonModule {
@Override
public String getModuleName() {
return "Spring Data MongoDB GeoJson - Serializers";
}
@Override
public Version version() {
return version;
}
@Override
public void setupModule(SetupContext ctx) {
final SimpleSerializers serializers = new SimpleSerializers();
serializers.addSerializer(GeoJsonPoint.class, new GeoJsonPointSerializer());
serializers.addSerializer(GeoJsonMultiPoint.class, new GeoJsonMultiPointSerializer());
serializers.addSerializer(GeoJsonLineString.class, new GeoJsonLineStringSerializer());
serializers.addSerializer(GeoJsonMultiLineString.class, new GeoJsonMultiLineStringSerializer());
serializers.addSerializer(GeoJsonPolygon.class, new GeoJsonPolygonSerializer());
serializers.addSerializer(GeoJsonMultiPolygon.class, new GeoJsonMultiPolygonSerializer());
ctx.addSerializers(serializers);
}
}
public static class Deserializers extends JacksonModule {
@Override
public String getModuleName() {
return "Spring Data MongoDB GeoJson - Deserializers";
}
@Override
public Version version() {
return version;
}
@Override
public void setupModule(SetupContext ctx) {
final SimpleDeserializers deserializers = new SimpleDeserializers();
deserializers.addDeserializer(GeoJsonPoint.class, new GeoJsonPointDeserializer());
deserializers.addDeserializer(GeoJsonMultiPoint.class, new GeoJsonMultiPointDeserializer());
deserializers.addDeserializer(GeoJsonLineString.class, new GeoJsonLineStringDeserializer());
deserializers.addDeserializer(GeoJsonMultiLineString.class, new GeoJsonMultiLineStringDeserializer());
deserializers.addDeserializer(GeoJsonPolygon.class, new GeoJsonPolygonDeserializer());
deserializers.addDeserializer(GeoJsonMultiPolygon.class, new GeoJsonMultiPolygonDeserializer());
ctx.addDeserializers(deserializers);
}
}
private abstract static class GeoJsonDeserializer<T extends GeoJson<?>> extends ValueDeserializer<T> {
public @Nullable T deserialize(JsonParser jp, @Nullable DeserializationContext context) throws JacksonException {
JsonNode node = jp.readValueAsTree();
JsonNode coordinates = node.get("coordinates");
return coordinates != null && coordinates.isArray() ? this.doDeserialize((ArrayNode) coordinates) : null;
}
protected abstract @Nullable T doDeserialize(ArrayNode coordinates);
protected @Nullable GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) {
return node == null ? null : new GeoJsonPoint(node.get(0).asDouble(), node.get(1).asDouble());
}
protected @Nullable Point toPoint(@Nullable ArrayNode node) {
return node == null ? null : new Point(node.get(0).asDouble(), node.get(1).asDouble());
}
protected List<Point> toPoints(@Nullable ArrayNode node) {
if (node == null) {
return Collections.emptyList();
} else {
List<Point> points = new ArrayList<>(node.size());
for (JsonNode coordinatePair : node) {
if (coordinatePair.isArray()) {
Point point = this.toPoint((ArrayNode) coordinatePair);
Assert.notNull(point, "Point must not be null!");
points.add(point);
}
}
return points;
}
}
protected GeoJsonLineString toLineString(ArrayNode node) {
return new GeoJsonLineString(this.toPoints(node));
}
}
private static class GeoJsonPointDeserializer extends GeoJsonDeserializer<GeoJsonPoint> {
protected @Nullable GeoJsonPoint doDeserialize(ArrayNode coordinates) {
return this.toGeoJsonPoint(coordinates);
}
}
private static class GeoJsonLineStringDeserializer extends GeoJsonDeserializer<GeoJsonLineString> {
protected GeoJsonLineString doDeserialize(ArrayNode coordinates) {
return new GeoJsonLineString(this.toPoints(coordinates));
}
}
private static class GeoJsonMultiPointDeserializer extends GeoJsonDeserializer<GeoJsonMultiPoint> {
protected GeoJsonMultiPoint doDeserialize(ArrayNode coordinates) {
return new GeoJsonMultiPoint(this.toPoints(coordinates));
}
}
private static class GeoJsonMultiLineStringDeserializer extends GeoJsonDeserializer<GeoJsonMultiLineString> {
protected GeoJsonMultiLineString doDeserialize(ArrayNode coordinates) {
List<GeoJsonLineString> lines = new ArrayList<>(coordinates.size());
for (JsonNode lineString : coordinates) {
if (lineString.isArray()) {
lines.add(this.toLineString((ArrayNode) lineString));
}
}
return new GeoJsonMultiLineString(lines);
}
}
private static class GeoJsonPolygonDeserializer extends GeoJsonDeserializer<GeoJsonPolygon> {
protected @Nullable GeoJsonPolygon doDeserialize(ArrayNode coordinates) {
Iterator<JsonNode> coordinateIterator = coordinates.iterator();
if (coordinateIterator.hasNext()) {
JsonNode ring = coordinateIterator.next();
return new GeoJsonPolygon(this.toPoints((ArrayNode) ring));
} else {
return null;
}
}
}
private static class GeoJsonMultiPolygonDeserializer extends GeoJsonDeserializer<GeoJsonMultiPolygon> {
protected GeoJsonMultiPolygon doDeserialize(ArrayNode coordinates) {
List<GeoJsonPolygon> polygons = new ArrayList<>(coordinates.size());
for (JsonNode polygon : coordinates) {
for (JsonNode ring : polygon) {
polygons.add(new GeoJsonPolygon(this.toPoints((ArrayNode) ring)));
}
}
return new GeoJsonMultiPolygon(polygons);
}
}
private abstract static class GeoJsonSerializer<T extends GeoJson<? extends Iterable<?>>> extends ValueSerializer<T> {
@Override
public void serialize(T shape, JsonGenerator jsonGenerator, SerializationContext context) {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringProperty("type", shape.getType());
jsonGenerator.writeArrayPropertyStart("coordinates");
this.doSerialize(shape, jsonGenerator);
jsonGenerator.writeEndArray();
jsonGenerator.writeEndObject();
}
protected abstract void doSerialize(T shape, JsonGenerator jsonGenerator);
protected void writePoint(Point point, JsonGenerator jsonGenerator) {
jsonGenerator.writeStartArray();
this.writeRawCoordinates(point, jsonGenerator);
jsonGenerator.writeEndArray();
}
protected void writeRawCoordinates(Point point, JsonGenerator jsonGenerator) {
jsonGenerator.writeNumber(point.getX());
jsonGenerator.writeNumber(point.getY());
}
protected void writeLine(Iterable<Point> points, JsonGenerator jsonGenerator) {
jsonGenerator.writeStartArray();
this.writeRawLine(points, jsonGenerator);
jsonGenerator.writeEndArray();
}
protected void writeRawLine(Iterable<Point> points, JsonGenerator jsonGenerator) {
for (Point point : points) {
this.writePoint(point, jsonGenerator);
}
}
}
static class GeoJsonPointSerializer extends GeoJsonSerializer<GeoJsonPoint> {
protected void doSerialize(GeoJsonPoint value, JsonGenerator jsonGenerator) {
this.writeRawCoordinates(value, jsonGenerator);
}
}
static class GeoJsonLineStringSerializer extends GeoJsonSerializer<GeoJsonLineString> {
protected void doSerialize(GeoJsonLineString value, JsonGenerator jsonGenerator) {
this.writeRawLine(value.getCoordinates(), jsonGenerator);
}
}
static class GeoJsonMultiPointSerializer extends GeoJsonSerializer<GeoJsonMultiPoint> {
protected void doSerialize(GeoJsonMultiPoint value, JsonGenerator jsonGenerator) {
this.writeRawLine(value.getCoordinates(), jsonGenerator);
}
}
static class GeoJsonMultiLineStringSerializer extends GeoJsonSerializer<GeoJsonMultiLineString> {
protected void doSerialize(GeoJsonMultiLineString value, JsonGenerator jsonGenerator) {
for (GeoJsonLineString lineString : value.getCoordinates()) {
this.writeLine(lineString.getCoordinates(), jsonGenerator);
}
}
}
static class GeoJsonPolygonSerializer extends GeoJsonSerializer<GeoJsonPolygon> {
protected void doSerialize(GeoJsonPolygon value, JsonGenerator jsonGenerator) throws JacksonException {
for (GeoJsonLineString lineString : value.getCoordinates()) {
this.writeLine(lineString.getCoordinates(), jsonGenerator);
}
}
}
static class GeoJsonMultiPolygonSerializer extends GeoJsonSerializer<GeoJsonMultiPolygon> {
protected void doSerialize(GeoJsonMultiPolygon value, JsonGenerator jsonGenerator) throws JacksonException {
for (GeoJsonPolygon polygon : value.getCoordinates()) {
jsonGenerator.writeStartArray();
for (GeoJsonLineString lineString : polygon.getCoordinates()) {
this.writeLine(lineString.getCoordinates(), jsonGenerator);
}
jsonGenerator.writeEndArray();
}
}
}
}

1
spring-data-mongodb/src/main/resources/META-INF/spring.factories

@ -1,2 +1,3 @@ @@ -1,2 +1,3 @@
org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.mongodb.config.GeoJsonConfiguration
org.springframework.data.web.config.SpringDataJackson3Modules=org.springframework.data.mongodb.config.GeoJsonJackson3Configuration
org.springframework.data.repository.core.support.RepositoryFactorySupport=org.springframework.data.mongodb.repository.support.MongoRepositoryFactory

13
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/GeoJsonConfigurationIntegrationTests.java → spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/GeoJsonJackson3ConfigurationIntegrationTests.java

@ -22,27 +22,30 @@ import org.junit.runner.RunWith; @@ -22,27 +22,30 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.geo.GeoJsonJackson3Module;
import org.springframework.data.mongodb.core.geo.GeoJsonModule;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration tests for {@link GeoJsonConfiguration}.
* Integration tests for {@link GeoJsonJackson3Configuration}.
*
* @author Oliver Gierke
* @author Bjorn Harvold
* @author Jens Schauder
*/
@RunWith(SpringRunner.class)
@ContextConfiguration
public class GeoJsonConfigurationIntegrationTests {
public class GeoJsonJackson3ConfigurationIntegrationTests {
@Configuration
@EnableSpringDataWebSupport
static class Config {}
@Autowired GeoJsonModule geoJsonModule;
@Autowired
GeoJsonJackson3Module geoJsonModule;
@Test // DATAMONGO-1181
@Test // GH-5100
public void picksUpGeoJsonModuleConfigurationByDefault() {
assertThat(geoJsonModule).isNotNull();
}

109
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonJackson3ModuleUnitTests.java

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.geo;
import static org.assertj.core.api.Assertions.*;
import tools.jackson.databind.json.JsonMapper;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.geo.Point;
/**
* Tests for the {@link GeoJsonModule}.
*
* @author Jens Schauder
*/
class GeoJsonJackson3ModuleUnitTests {
JsonMapper mapper;
@BeforeEach
void setUp() {
mapper = JsonMapper.builder()
.addModule(new GeoJsonJackson3Module.Serializers())
.addModule(new GeoJsonJackson3Module.Deserializers())
.build();
}
@Test // GH-5100
void shouldDeserializeJsonPointCorrectly() {
String json = "{ \"type\": \"Point\", \"coordinates\": [10.0, 20.0] }";
assertThat(mapper.readValue(json, GeoJsonPoint.class)).isEqualTo(new GeoJsonPoint(10D, 20D));
}
@Test // GH-5100
void shouldDeserializeGeoJsonLineStringCorrectly() {
String json = "{ \"type\": \"LineString\", \"coordinates\": [ [10.0, 20.0], [30.0, 40.0], [50.0, 60.0] ]}";
assertThat(mapper.readValue(json, GeoJsonLineString.class))
.isEqualTo(new GeoJsonLineString(Arrays.asList(new Point(10, 20), new Point(30, 40), new Point(50, 60))));
}
@Test // GH-5100
void shouldDeserializeGeoJsonMultiPointCorrectly() {
String json = "{ \"type\": \"MultiPoint\", \"coordinates\": [ [10.0, 20.0], [30.0, 40.0], [50.0, 60.0] ]}";
assertThat(mapper.readValue(json, GeoJsonLineString.class))
.isEqualTo(new GeoJsonMultiPoint(Arrays.asList(new Point(10, 20), new Point(30, 40), new Point(50, 60))));
}
@Test // GH-5100
@SuppressWarnings("unchecked")
void shouldDeserializeGeoJsonMultiLineStringCorrectly() {
String json = "{ \"type\": \"MultiLineString\", \"coordinates\": [ [ [10.0, 20.0], [30.0, 40.0] ], [ [50.0, 60.0] , [70.0, 80.0] ] ]}";
assertThat(mapper.readValue(json, GeoJsonMultiLineString.class)).isEqualTo(new GeoJsonMultiLineString(
Arrays.asList(new Point(10, 20), new Point(30, 40)), Arrays.asList(new Point(50, 60), new Point(70, 80))));
}
@Test // GH-5100
void shouldDeserializeGeoJsonPolygonCorrectly() {
String json = "{ \"type\": \"Polygon\", \"coordinates\": [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] ]}";
assertThat(mapper.readValue(json, GeoJsonPolygon.class)).isEqualTo(new GeoJsonPolygon(
Arrays.asList(new Point(100, 0), new Point(101, 0), new Point(101, 1), new Point(100, 1), new Point(100, 0))));
}
@Test // GH-5100
void shouldDeserializeGeoJsonMultiPolygonCorrectly() {
String json = "{ \"type\": \"Polygon\", \"coordinates\": ["
+ "[[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]],"
+ "[[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]],"
+ "[[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]"//
+ "]}";
assertThat(mapper.readValue(json, GeoJsonMultiPolygon.class)).isEqualTo(new GeoJsonMultiPolygon(Arrays.asList(
new GeoJsonPolygon(Arrays.asList(new Point(102, 2), new Point(103, 2), new Point(103, 3), new Point(102, 3),
new Point(102, 2))),
new GeoJsonPolygon(Arrays.asList(new Point(100, 0), new Point(101, 0), new Point(101, 1), new Point(100, 1),
new Point(100, 0))),
new GeoJsonPolygon(Arrays.asList(new Point(100.2, 0.2), new Point(100.8, 0.2), new Point(100.8, 0.8),
new Point(100.2, 0.8), new Point(100.2, 0.2))))));
}
}

18
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonModuleUnitTests.java

@ -30,21 +30,23 @@ import com.fasterxml.jackson.databind.JsonMappingException; @@ -30,21 +30,23 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Tests for the {@link GeoJsonModule}
*
* @author Christoph Strobl
*/
public class GeoJsonModuleUnitTests {
class GeoJsonModuleUnitTests {
ObjectMapper mapper;
@BeforeEach
public void setUp() {
void setUp() {
mapper = new ObjectMapper();
mapper.registerModule(new GeoJsonModule());
}
@Test // DATAMONGO-1181
public void shouldDeserializeJsonPointCorrectly() throws JsonParseException, JsonMappingException, IOException {
void shouldDeserializeJsonPointCorrectly() throws JsonParseException, JsonMappingException, IOException {
String json = "{ \"type\": \"Point\", \"coordinates\": [10.0, 20.0] }";
@ -52,7 +54,7 @@ public class GeoJsonModuleUnitTests { @@ -52,7 +54,7 @@ public class GeoJsonModuleUnitTests {
}
@Test // DATAMONGO-1181
public void shouldDeserializeGeoJsonLineStringCorrectly()
void shouldDeserializeGeoJsonLineStringCorrectly()
throws JsonParseException, JsonMappingException, IOException {
String json = "{ \"type\": \"LineString\", \"coordinates\": [ [10.0, 20.0], [30.0, 40.0], [50.0, 60.0] ]}";
@ -62,7 +64,7 @@ public class GeoJsonModuleUnitTests { @@ -62,7 +64,7 @@ public class GeoJsonModuleUnitTests {
}
@Test // DATAMONGO-1181
public void shouldDeserializeGeoJsonMultiPointCorrectly()
void shouldDeserializeGeoJsonMultiPointCorrectly()
throws JsonParseException, JsonMappingException, IOException {
String json = "{ \"type\": \"MultiPoint\", \"coordinates\": [ [10.0, 20.0], [30.0, 40.0], [50.0, 60.0] ]}";
@ -73,7 +75,7 @@ public class GeoJsonModuleUnitTests { @@ -73,7 +75,7 @@ public class GeoJsonModuleUnitTests {
@Test // DATAMONGO-1181
@SuppressWarnings("unchecked")
public void shouldDeserializeGeoJsonMultiLineStringCorrectly()
void shouldDeserializeGeoJsonMultiLineStringCorrectly()
throws JsonParseException, JsonMappingException, IOException {
String json = "{ \"type\": \"MultiLineString\", \"coordinates\": [ [ [10.0, 20.0], [30.0, 40.0] ], [ [50.0, 60.0] , [70.0, 80.0] ] ]}";
@ -83,7 +85,7 @@ public class GeoJsonModuleUnitTests { @@ -83,7 +85,7 @@ public class GeoJsonModuleUnitTests {
}
@Test // DATAMONGO-1181
public void shouldDeserializeGeoJsonPolygonCorrectly() throws JsonParseException, JsonMappingException, IOException {
void shouldDeserializeGeoJsonPolygonCorrectly() throws JsonParseException, JsonMappingException, IOException {
String json = "{ \"type\": \"Polygon\", \"coordinates\": [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] ]}";
@ -92,7 +94,7 @@ public class GeoJsonModuleUnitTests { @@ -92,7 +94,7 @@ public class GeoJsonModuleUnitTests {
}
@Test // DATAMONGO-1181
public void shouldDeserializeGeoJsonMultiPolygonCorrectly()
void shouldDeserializeGeoJsonMultiPolygonCorrectly()
throws JsonParseException, JsonMappingException, IOException {
String json = "{ \"type\": \"Polygon\", \"coordinates\": ["

Loading…
Cancel
Save