GeoJSON Parsing in Java: A Comprehensive Guide

GeoJSON is a popular format for encoding geographic data structures. This guide covers parsing, creating, and manipulating GeoJSON data in Java using various libraries and techniques.


Core GeoJSON Concepts

What is GeoJSON?

  • A standard format for representing geographic features
  • Based on JSON format
  • Supports Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection
  • Can include properties for each feature

Key GeoJSON Types:

  • Point: Single coordinate
  • LineString: Sequence of coordinates
  • Polygon: Sequence of linear rings
  • Feature: Geometry with properties
  • FeatureCollection: Collection of features

Dependencies and Setup

Maven Dependencies
<properties>
<jackson.version>2.15.2</jackson.version>
<geotools.version>28.2</geotools.version>
<jts.version>1.19.0</jts.version>
</properties>
<dependencies>
<!-- Jackson for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- JTS Topology Suite -->
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>${jts.version}</version>
</dependency>
<!-- GeoTools (optional, for advanced GIS operations) -->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geojson</artifactId>
<version>${geotools.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Repository for GeoTools
<repositories>
<repository>
<id>osgeo</id>
<name>OSGeo Release Repository</name>
<url>https://repo.osgeo.org/repository/release/</url>
</repository>
</repositories>

Custom GeoJSON Model Classes

1. Base Geometry Interface and Classes
public interface Geometry {
String getType();
BoundingBox getBbox();
}
public class BoundingBox {
private final double minX;
private final double minY;
private final double maxX;
private final double maxY;
public BoundingBox(double minX, double minY, double maxX, double maxY) {
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
// Getters
public double getMinX() { return minX; }
public double getMinY() { return minY; }
public double getMaxX() { return maxX; }
public double getMaxY() { return maxY; }
@Override
public String toString() {
return String.format("[%.6f, %.6f, %.6f, %.6f]", minX, minY, maxX, maxY);
}
}
public class Coordinate {
private final double x; // longitude
private final double y; // latitude
private final Double z; // elevation (optional)
public Coordinate(double x, double y) {
this(x, y, null);
}
public Coordinate(double x, double y, Double z) {
this.x = x;
this.y = y;
this.z = z;
}
// Getters
public double getX() { return x; }
public double getY() { return y; }
public Optional<Double> getZ() { return Optional.ofNullable(z); }
public double getLongitude() { return x; }
public double getLatitude() { return y; }
@Override
public String toString() {
if (z != null) {
return String.format("[%.6f, %.6f, %.6f]", x, y, z);
}
return String.format("[%.6f, %.6f]", x, y);
}
}
2. Geometry Implementations
public class Point implements Geometry {
private final Coordinate coordinates;
private final BoundingBox bbox;
public Point(Coordinate coordinates) {
this(coordinates, null);
}
public Point(Coordinate coordinates, BoundingBox bbox) {
this.coordinates = coordinates;
this.bbox = bbox;
}
@Override
public String getType() {
return "Point";
}
public Coordinate getCoordinates() {
return coordinates;
}
@Override
public BoundingBox getBbox() {
return bbox;
}
}
public class LineString implements Geometry {
private final List<Coordinate> coordinates;
private final BoundingBox bbox;
public LineString(List<Coordinate> coordinates) {
this(coordinates, null);
}
public LineString(List<Coordinate> coordinates, BoundingBox bbox) {
if (coordinates.size() < 2) {
throw new IllegalArgumentException("LineString must have at least 2 coordinates");
}
this.coordinates = new ArrayList<>(coordinates);
this.bbox = bbox;
}
@Override
public String getType() {
return "LineString";
}
public List<Coordinate> getCoordinates() {
return Collections.unmodifiableList(coordinates);
}
@Override
public BoundingBox getBbox() {
return bbox;
}
}
public class Polygon implements Geometry {
private final List<List<Coordinate>> coordinates;
private final BoundingBox bbox;
public Polygon(List<List<Coordinate>> coordinates) {
this(coordinates, null);
}
public Polygon(List<List<Coordinate>> coordinates, BoundingBox bbox) {
validatePolygon(coordinates);
this.coordinates = deepCopyCoordinates(coordinates);
this.bbox = bbox;
}
private void validatePolygon(List<List<Coordinate>> coordinates) {
if (coordinates.isEmpty()) {
throw new IllegalArgumentException("Polygon must have at least one ring");
}
// First ring is exterior, others are interior (holes)
for (List<Coordinate> ring : coordinates) {
if (ring.size() < 4) {
throw new IllegalArgumentException("Polygon ring must have at least 4 coordinates");
}
// Check if ring is closed (first and last coordinate are equal)
Coordinate first = ring.get(0);
Coordinate last = ring.get(ring.size() - 1);
if (!first.equals(last)) {
throw new IllegalArgumentException("Polygon ring must be closed");
}
}
}
private List<List<Coordinate>> deepCopyCoordinates(List<List<Coordinate>> original) {
return original.stream()
.map(ArrayList::new)
.collect(Collectors.toList());
}
@Override
public String getType() {
return "Polygon";
}
public List<List<Coordinate>> getCoordinates() {
return Collections.unmodifiableList(
coordinates.stream()
.map(Collections::unmodifiableList)
.collect(Collectors.toList())
);
}
public List<Coordinate> getExteriorRing() {
return coordinates.get(0);
}
public List<List<Coordinate>> getInteriorRings() {
return coordinates.subList(1, coordinates.size());
}
@Override
public BoundingBox getBbox() {
return bbox;
}
}
3. Feature and FeatureCollection
public class Feature {
private final String type = "Feature";
private final Geometry geometry;
private final Map<String, Object> properties;
private final String id;
private final BoundingBox bbox;
public Feature(Geometry geometry) {
this(geometry, null, null, null);
}
public Feature(Geometry geometry, Map<String, Object> properties) {
this(geometry, properties, null, null);
}
public Feature(Geometry geometry, Map<String, Object> properties, String id, BoundingBox bbox) {
this.geometry = geometry;
this.properties = properties != null ? new HashMap<>(properties) : new HashMap<>();
this.id = id;
this.bbox = bbox;
}
// Getters
public String getType() { return type; }
public Geometry getGeometry() { return geometry; }
public Map<String, Object> getProperties() { return Collections.unmodifiableMap(properties); }
public String getId() { return id; }
public BoundingBox getBbox() { return bbox; }
public void setProperty(String key, Object value) {
properties.put(key, value);
}
public Object getProperty(String key) {
return properties.get(key);
}
}
public class FeatureCollection {
private final String type = "FeatureCollection";
private final List<Feature> features;
private final BoundingBox bbox;
public FeatureCollection(List<Feature> features) {
this(features, null);
}
public FeatureCollection(List<Feature> features, BoundingBox bbox) {
this.features = new ArrayList<>(features);
this.bbox = bbox;
}
// Getters
public String getType() { return type; }
public List<Feature> getFeatures() { return Collections.unmodifiableList(features); }
public BoundingBox getBbox() { return bbox; }
public void addFeature(Feature feature) {
features.add(feature);
}
public boolean removeFeature(Feature feature) {
return features.remove(feature);
}
}

Jackson JSON Parsing

1. Custom Jackson Deserializers
public class CoordinateDeserializer extends JsonDeserializer<Coordinate> {
@Override
public Coordinate deserialize(JsonParser p, DeserializationContext ctxt) 
throws IOException {
JsonNode node = p.getCodec().readTree(p);
if (!node.isArray()) {
throw new IllegalArgumentException("Coordinate must be an array");
}
int size = node.size();
if (size < 2 || size > 3) {
throw new IllegalArgumentException("Coordinate array must have 2 or 3 elements");
}
double x = node.get(0).asDouble();
double y = node.get(1).asDouble();
Double z = (size == 3) ? node.get(2).asDouble() : null;
return new Coordinate(x, y, z);
}
}
public class GeometryDeserializer extends JsonDeserializer<Geometry> {
@Override
public Geometry deserialize(JsonParser p, DeserializationContext ctxt) 
throws IOException {
JsonNode node = p.getCodec().readTree(p);
String type = node.get("type").asText();
JsonNode coordinatesNode = node.get("coordinates");
JsonNode bboxNode = node.get("bbox");
BoundingBox bbox = parseBbox(bboxNode);
switch (type) {
case "Point":
Coordinate pointCoord = parseCoordinate(coordinatesNode);
return new Point(pointCoord, bbox);
case "LineString":
List<Coordinate> lineCoords = parseCoordinateArray(coordinatesNode);
return new LineString(lineCoords, bbox);
case "Polygon":
List<List<Coordinate>> polygonCoords = parsePolygonCoordinates(coordinatesNode);
return new Polygon(polygonCoords, bbox);
default:
throw new IllegalArgumentException("Unsupported geometry type: " + type);
}
}
private Coordinate parseCoordinate(JsonNode node) {
// Implementation for parsing single coordinate
return null; // Simplified for example
}
private List<Coordinate> parseCoordinateArray(JsonNode node) {
// Implementation for parsing coordinate array
return null; // Simplified for example
}
private List<List<Coordinate>> parsePolygonCoordinates(JsonNode node) {
// Implementation for parsing polygon coordinates
return null; // Simplified for example
}
private BoundingBox parseBbox(JsonNode bboxNode) {
if (bboxNode == null || !bboxNode.isArray()) {
return null;
}
if (bboxNode.size() != 4) {
throw new IllegalArgumentException("Bounding box must have exactly 4 elements");
}
double minX = bboxNode.get(0).asDouble();
double minY = bboxNode.get(1).asDouble();
double maxX = bboxNode.get(2).asDouble();
double maxY = bboxNode.get(3).asDouble();
return new BoundingBox(minX, minY, maxX, maxY);
}
}
2. Jackson Mixin for Annotations
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Point.class, name = "Point"),
@JsonSubTypes.Type(value = LineString.class, name = "LineString"),
@JsonSubTypes.Type(value = Polygon.class, name = "Polygon"),
@JsonSubTypes.Type(value = Feature.class, name = "Feature"),
@JsonSubTypes.Type(value = FeatureCollection.class, name = "FeatureCollection")
})
public abstract class GeometryMixin {
}
@JsonDeserialize(using = CoordinateDeserializer.class)
abstract class CoordinateMixin {
}
abstract class FeatureMixin {
@JsonProperty("type")
public abstract String getType();
@JsonProperty("geometry")
public abstract Geometry getGeometry();
@JsonProperty("properties")
public abstract Map<String, Object> getProperties();
@JsonProperty("id")
public abstract String getId();
@JsonProperty("bbox")
public abstract BoundingBox getBbox();
}
3. GeoJSON Parser Service
@Service
public class GeoJsonParser {
private final ObjectMapper objectMapper;
public GeoJsonParser() {
this.objectMapper = new ObjectMapper();
configureObjectMapper();
}
private void configureObjectMapper() {
// Register mixins
objectMapper.addMixIn(Geometry.class, GeometryMixin.class);
objectMapper.addMixIn(Coordinate.class, CoordinateMixin.class);
objectMapper.addMixIn(Feature.class, FeatureMixin.class);
// Configure features
objectMapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
public FeatureCollection parseFeatureCollection(String geoJson) throws IOException {
return objectMapper.readValue(geoJson, FeatureCollection.class);
}
public Feature parseFeature(String geoJson) throws IOException {
return objectMapper.readValue(geoJson, Feature.class);
}
public Geometry parseGeometry(String geoJson) throws IOException {
return objectMapper.readValue(geoJson, Geometry.class);
}
public String toGeoJson(Object geoJsonObject) throws IOException {
return objectMapper.writeValueAsString(geoJsonObject);
}
public String toPrettyGeoJson(Object geoJsonObject) throws IOException {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(geoJsonObject);
}
}

JTS Integration

1. JTS Geometry Converter
@Component
public class JtsGeometryConverter {
public org.locationtech.jts.geom.Geometry toJtsGeometry(Geometry geoJsonGeometry) {
GeometryFactory geometryFactory = new GeometryFactory();
if (geoJsonGeometry instanceof Point) {
return convertPoint((Point) geoJsonGeometry, geometryFactory);
} else if (geoJsonGeometry instanceof LineString) {
return convertLineString((LineString) geoJsonGeometry, geometryFactory);
} else if (geoJsonGeometry instanceof Polygon) {
return convertPolygon((Polygon) geoJsonGeometry, geometryFactory);
} else {
throw new IllegalArgumentException("Unsupported geometry type: " + 
geoJsonGeometry.getClass().getSimpleName());
}
}
private org.locationtech.jts.geom.Point convertPoint(Point point, GeometryFactory factory) {
Coordinate coordinate = point.getCoordinates();
return factory.createPoint(
new org.locationtech.jts.geom.Coordinate(
coordinate.getX(), 
coordinate.getY(),
coordinate.getZ().orElse(Double.NaN)
)
);
}
private org.locationtech.jts.geom.LineString convertLineString(LineString lineString, GeometryFactory factory) {
org.locationtech.jts.geom.Coordinate[] jtsCoords = lineString.getCoordinates().stream()
.map(coord -> new org.locationtech.jts.geom.Coordinate(coord.getX(), coord.getY()))
.toArray(org.locationtech.jts.geom.Coordinate[]::new);
return factory.createLineString(jtsCoords);
}
private org.locationtech.jts.geom.Polygon convertPolygon(Polygon polygon, GeometryFactory factory) {
org.locationtech.jts.geom.LinearRing exterior = convertLinearRing(
polygon.getExteriorRing(), factory);
org.locationtech.jts.geom.LinearRing[] interiors = polygon.getInteriorRings().stream()
.map(ring -> convertLinearRing(ring, factory))
.toArray(org.locationtech.jts.geom.LinearRing[]::new);
return factory.createPolygon(exterior, interiors);
}
private org.locationtech.jts.geom.LinearRing convertLinearRing(List<Coordinate> coordinates, GeometryFactory factory) {
org.locationtech.jts.geom.Coordinate[] jtsCoords = coordinates.stream()
.map(coord -> new org.locationtech.jts.geom.Coordinate(coord.getX(), coord.getY()))
.toArray(org.locationtech.jts.geom.Coordinate[]::new);
return factory.createLinearRing(jtsCoords);
}
public Geometry fromJtsGeometry(org.locationtech.jts.geom.Geometry jtsGeometry) {
if (jtsGeometry instanceof org.locationtech.jts.geom.Point) {
return convertJtsPoint((org.locationtech.jts.geom.Point) jtsGeometry);
} else if (jtsGeometry instanceof org.locationtech.jts.geom.LineString) {
return convertJtsLineString((org.locationtech.jts.geom.LineString) jtsGeometry);
} else if (jtsGeometry instanceof org.locationtech.jts.geom.Polygon) {
return convertJtsPolygon((org.locationtech.jts.geom.Polygon) jtsGeometry);
} else {
throw new IllegalArgumentException("Unsupported JTS geometry type: " + 
jtsGeometry.getClass().getSimpleName());
}
}
private Point convertJtsPoint(org.locationtech.jts.geom.Point jtsPoint) {
org.locationtech.jts.geom.Coordinate jtsCoord = jtsPoint.getCoordinate();
Coordinate coord = new Coordinate(jtsCoord.x, jtsCoord.y);
return new Point(coord);
}
// Similar conversion methods for other geometry types...
}
2. Spatial Operations Service
@Service
public class SpatialOperationsService {
private final JtsGeometryConverter geometryConverter;
public SpatialOperationsService(JtsGeometryConverter geometryConverter) {
this.geometryConverter = geometryConverter;
}
public boolean contains(Geometry geometry1, Geometry geometry2) {
org.locationtech.jts.geom.Geometry jtsGeo1 = geometryConverter.toJtsGeometry(geometry1);
org.locationtech.jts.geom.Geometry jtsGeo2 = geometryConverter.toJtsGeometry(geometry2);
return jtsGeo1.contains(jtsGeo2);
}
public boolean intersects(Geometry geometry1, Geometry geometry2) {
org.locationtech.jts.geom.Geometry jtsGeo1 = geometryConverter.toJtsGeometry(geometry1);
org.locationtech.jts.geom.Geometry jtsGeo2 = geometryConverter.toJtsGeometry(geometry2);
return jtsGeo1.intersects(jtsGeo2);
}
public double calculateArea(Polygon polygon) {
org.locationtech.jts.geom.Geometry jtsGeometry = geometryConverter.toJtsGeometry(polygon);
return jtsGeometry.getArea();
}
public double calculateLength(Geometry geometry) {
org.locationtech.jts.geom.Geometry jtsGeometry = geometryConverter.toJtsGeometry(geometry);
return jtsGeometry.getLength();
}
public Geometry calculateBuffer(Geometry geometry, double distance) {
org.locationtech.jts.geom.Geometry jtsGeometry = geometryConverter.toJtsGeometry(geometry);
org.locationtech.jts.geom.Geometry buffered = jtsGeometry.buffer(distance);
return geometryConverter.fromJtsGeometry(buffered);
}
}

Usage Examples

1. Basic GeoJSON Creation and Parsing
@Service
public class GeoJsonService {
private final GeoJsonParser geoJsonParser;
private final SpatialOperationsService spatialOps;
public GeoJsonService(GeoJsonParser geoJsonParser, SpatialOperationsService spatialOps) {
this.geoJsonParser = geoJsonParser;
this.spatialOps = spatialOps;
}
public String createSampleFeatureCollection() throws IOException {
// Create points
Point empireState = new Point(new Coordinate(-73.9857, 40.7484));
Point timesSquare = new Point(new Coordinate(-73.9855, 40.7580));
// Create properties
Map<String, Object> empireStateProps = new HashMap<>();
empireStateProps.put("name", "Empire State Building");
empireStateProps.put("height", 443.2);
Map<String, Object> timesSquareProps = new HashMap<>();
timesSquareProps.put("name", "Times Square");
timesSquareProps.put("description", "Major commercial intersection");
// Create features
Feature empireStateFeature = new Feature(empireState, empireStateProps, "1", null);
Feature timesSquareFeature = new Feature(timesSquare, timesSquareProps, "2", null);
// Create feature collection
FeatureCollection featureCollection = new FeatureCollection(
Arrays.asList(empireStateFeature, timesSquareFeature)
);
return geoJsonParser.toPrettyGeoJson(featureCollection);
}
public void parseAndAnalyzeGeoJson(String geoJson) throws IOException {
FeatureCollection featureCollection = geoJsonParser.parseFeatureCollection(geoJson);
System.out.println("Found " + featureCollection.getFeatures().size() + " features");
for (Feature feature : featureCollection.getFeatures()) {
Geometry geometry = feature.getGeometry();
Map<String, Object> properties = feature.getProperties();
System.out.println("Feature ID: " + feature.getId());
System.out.println("Geometry Type: " + geometry.getType());
System.out.println("Properties: " + properties);
if (geometry instanceof Polygon) {
double area = spatialOps.calculateArea((Polygon) geometry);
System.out.println("Polygon Area: " + area + " square units");
}
}
}
public FeatureCollection filterFeaturesByBoundingBox(FeatureCollection features, 
BoundingBox bbox) {
List<Feature> filtered = features.getFeatures().stream()
.filter(feature -> isFeatureInBoundingBox(feature, bbox))
.collect(Collectors.toList());
return new FeatureCollection(filtered);
}
private boolean isFeatureInBoundingBox(Feature feature, BoundingBox bbox) {
// Simplified implementation - in reality, you'd use proper spatial indexing
Geometry geometry = feature.getGeometry();
BoundingBox featureBbox = geometry.getBbox();
if (featureBbox == null) {
// Calculate bbox from geometry coordinates
featureBbox = calculateBoundingBox(geometry);
}
return featureBbox.getMinX() >= bbox.getMinX() &&
featureBbox.getMaxX() <= bbox.getMaxX() &&
featureBbox.getMinY() >= bbox.getMinY() &&
featureBbox.getMaxY() <= bbox.getMaxY();
}
private BoundingBox calculateBoundingBox(Geometry geometry) {
// Implementation to calculate bounding box from geometry coordinates
return null; // Simplified for example
}
}
2. REST Controller for GeoJSON API
@RestController
@RequestMapping("/api/geojson")
public class GeoJsonController {
private final GeoJsonService geoJsonService;
public GeoJsonController(GeoJsonService geoJsonService) {
this.geoJsonService = geoJsonService;
}
@GetMapping("/sample")
public ResponseEntity<String> getSampleGeoJson() {
try {
String geoJson = geoJsonService.createSampleFeatureCollection();
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(geoJson);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/analyze")
public ResponseEntity<Map<String, Object>> analyzeGeoJson(@RequestBody String geoJson) {
try {
Map<String, Object> analysis = new HashMap<>();
FeatureCollection featureCollection = geoJsonService.parseFeatureCollection(geoJson);
analysis.put("featureCount", featureCollection.getFeatures().size());
analysis.put("geometryTypes", featureCollection.getFeatures().stream()
.map(f -> f.getGeometry().getType())
.distinct()
.collect(Collectors.toList()));
return ResponseEntity.ok(analysis);
} catch (IOException e) {
return ResponseEntity.badRequest().build();
}
}
@PostMapping("/spatial/contains")
public ResponseEntity<Boolean> checkContains(@RequestBody SpatialQuery query) {
try {
Geometry geometry1 = geoJsonService.parseGeometry(query.getGeometry1());
Geometry geometry2 = geoJsonService.parseGeometry(query.getGeometry2());
boolean contains = geoJsonService.getSpatialOps().contains(geometry1, geometry2);
return ResponseEntity.ok(contains);
} catch (IOException e) {
return ResponseEntity.badRequest().build();
}
}
}
3. GeoJSON Validation
@Component
public class GeoJsonValidator {
public ValidationResult validateGeoJson(String geoJson) {
List<String> errors = new ArrayList<>();
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(geoJson);
if (!root.has("type")) {
errors.add("Missing 'type' field");
return new ValidationResult(false, errors);
}
String type = root.get("type").asText();
switch (type) {
case "FeatureCollection":
validateFeatureCollection(root, errors);
break;
case "Feature":
validateFeature(root, errors);
break;
case "Point":
case "LineString":
case "Polygon":
validateGeometry(root, errors);
break;
default:
errors.add("Unknown type: " + type);
}
} catch (IOException e) {
errors.add("Invalid JSON: " + e.getMessage());
}
return new ValidationResult(errors.isEmpty(), errors);
}
private void validateFeatureCollection(JsonNode node, List<String> errors) {
if (!node.has("features") || !node.get("features").isArray()) {
errors.add("FeatureCollection must have 'features' array");
return;
}
JsonNode features = node.get("features");
for (int i = 0; i < features.size(); i++) {
validateFeature(features.get(i), errors);
}
}
private void validateFeature(JsonNode node, List<String> errors) {
if (!node.has("geometry")) {
errors.add("Feature must have 'geometry'");
} else {
validateGeometry(node.get("geometry"), errors);
}
if (node.has("properties") && !node.get("properties").isObject()) {
errors.add("Feature properties must be an object");
}
}
private void validateGeometry(JsonNode node, List<String> errors) {
if (!node.has("type")) {
errors.add("Geometry must have 'type'");
return;
}
if (!node.has("coordinates")) {
errors.add("Geometry must have 'coordinates'");
return;
}
String type = node.get("type").asText();
JsonNode coordinates = node.get("coordinates");
switch (type) {
case "Point":
validatePointCoordinates(coordinates, errors);
break;
case "LineString":
validateLineStringCoordinates(coordinates, errors);
break;
case "Polygon":
validatePolygonCoordinates(coordinates, errors);
break;
}
}
private void validatePointCoordinates(JsonNode coordinates, List<String> errors) {
if (!coordinates.isArray() || (coordinates.size() != 2 && coordinates.size() != 3)) {
errors.add("Point coordinates must be array of 2 or 3 numbers");
}
}
// Similar validation methods for other geometry types...
public static class ValidationResult {
private final boolean valid;
private final List<String> errors;
public ValidationResult(boolean valid, List<String> errors) {
this.valid = valid;
this.errors = errors;
}
// Getters
public boolean isValid() { return valid; }
public List<String> getErrors() { return Collections.unmodifiableList(errors); }
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class GeoJsonParserTest {
@InjectMocks
private GeoJsonParser geoJsonParser;
@Test
void shouldParsePointGeometry() throws IOException {
// Given
String pointJson = """
{
"type": "Point",
"coordinates": [100.0, 0.0]
}
""";
// When
Geometry geometry = geoJsonParser.parseGeometry(pointJson);
// Then
assertThat(geometry).isInstanceOf(Point.class);
Point point = (Point) geometry;
assertThat(point.getCoordinates().getX()).isEqualTo(100.0);
assertThat(point.getCoordinates().getY()).isEqualTo(0.0);
}
@Test
void shouldParseFeatureCollection() throws IOException {
// Given
String featureCollectionJson = """
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [100.0, 0.0]
},
"properties": {
"name": "Test Point"
}
}
]
}
""";
// When
FeatureCollection featureCollection = geoJsonParser.parseFeatureCollection(featureCollectionJson);
// Then
assertThat(featureCollection.getFeatures()).hasSize(1);
Feature feature = featureCollection.getFeatures().get(0);
assertThat(feature.getProperty("name")).isEqualTo("Test Point");
}
}
@SpringBootTest
class SpatialOperationsServiceTest {
@Autowired
private SpatialOperationsService spatialOps;
@Test
void shouldCalculatePolygonArea() {
// Given
Polygon polygon = createSamplePolygon();
// When
double area = spatialOps.calculateArea(polygon);
// Then
assertThat(area).isPositive();
}
private Polygon createSamplePolygon() {
List<Coordinate> ring = Arrays.asList(
new Coordinate(0, 0),
new Coordinate(10, 0),
new Coordinate(10, 10),
new Coordinate(0, 10),
new Coordinate(0, 0) // Close the ring
);
return new Polygon(Collections.singletonList(ring));
}
}

Best Practices

  1. Coordinate Order: Always use [longitude, latitude] order (GeoJSON standard)
  2. Precision: Be mindful of floating-point precision for coordinates
  3. Validation: Always validate GeoJSON before processing
  4. Memory Management: Large GeoJSON files can be memory-intensive
  5. Error Handling: Provide clear error messages for malformed GeoJSON
  6. Performance: Use spatial indexing for large datasets
// Example of streaming processing for large GeoJSON files
@Component
public class StreamingGeoJsonProcessor {
public void processLargeGeoJson(InputStream geoJsonStream, Consumer<Feature> featureProcessor) 
throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonParser parser = mapper.getFactory().createParser(geoJsonStream);
// Navigate to features array
while (parser.nextToken() != null) {
if ("features".equals(parser.getCurrentName())) {
parser.nextToken(); // Move to START_ARRAY
// Process each feature individually
while (parser.nextToken() != JsonToken.END_ARRAY) {
Feature feature = mapper.readValue(parser, Feature.class);
featureProcessor.accept(feature);
}
break;
}
}
parser.close();
}
}

Conclusion

GeoJSON parsing in Java enables:

  • Spatial data processing for geographic applications
  • Integration with mapping libraries and GIS systems
  • Spatial analysis and geometric operations
  • Standardized data exchange between systems

By using the patterns and libraries shown above, you can effectively work with GeoJSON data in Java applications, from simple parsing to complex spatial operations and validation. The combination of Jackson for JSON processing and JTS for spatial operations provides a robust foundation for building geographic applications.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper