Skip to content

DHI.Spatial.GeoJson — Internal Developer Guide

This package provides System.Text.Json converters to serialize/deserialize our DHI.Spatial types (geometries, features, feature collections, attributes, CRS, etc.) to and from GeoJSON-structured payloads with DHI extensions. It also includes a few utility converters used across our spatial stack.

Note: our output follows the GeoJSON structure for Feature/FeatureCollection/Geometry, with DHI extensions (attributes, crs object shape, type metadata). Property name casing follows the PropertyNamingPolicy set on your JsonSerializerOptions — configure JsonNamingPolicy.CamelCase if strict RFC 7946 lowercase keys are required.


TL;DR — What you get

  • Serialize/deserialize:
    • IGeometry (Point, LineString, Polygon, Multi*, GeometryCollection)
    • IFeature and IFeatureCollection
    • BoundingBox, CoordinateReferenceSystem, FeatureInfo
    • IAttribute and IAssociation
  • Robust numeric formatting for coordinates (no scientific notation; 1 emitted as 1.0).
  • “Smart” object value handling (numbers, booleans, DateTime, Guid) via an inferred-type converter.
  • A safe Type ↔ string converter for metadata.

Typical setup

Add the converters you need to your JsonSerializerOptions. You usually don’t need a global camelCase policy: the converters write the correct property names themselves.

var jsonOptions = new JsonSerializerOptions
{
    // If you globally enforce camelCase elsewhere, that's fine too.
    // PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.GeometryConverter());
jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.GeometryCollectionConverter());
jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.FeatureConverter());
jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.FeatureCollectionConverter());
jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.FeatureInfoConverter());
jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.AttributeConverter());
jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.AssociationConverter());
jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.BoundingBoxConverter());
jsonOptions.Converters.Add(new DHI.Spatial.GeoJson.CoordinateReferenceSystemConverter());

JSON shapes (writer output)

Geometry

We emit standard GeoJSON geometry objects plus an optional CRS object.

{
  "type": "Polygon",
  "coordinates": [
    [[0.0, 0.0],[10.0, 0.0],[10.0, 5.0],[0.0, 5.0],[0.0, 0.0]],
    [[2.0, 1.0],[3.0, 1.0],[3.0, 2.0],[2.0, 2.0],[2.0, 1.0]]
  ],
  "crs": {
    "type": "name",
    "properties": { "name": "EPSG:4326" }
  }
}
  • Supported type values: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon.
  • Coordinates are arrays of [x, y] or [x, y, z] (z only written when Position.Z is set).
  • Numbers are formatted without scientific notation; integers become e.g. 1.0 (see DoubleConverter below).

Feature

{
  "type": "Feature",
  "geometry": { ...geometry... },
  "properties": {
    "name": "cell-23",
    "elevation": 12.5,
    "timestamp": "2024-10-01T12:00:00Z"
  }
}
  • properties is a Dictionary<string, object?>. Values are inferred at runtime:
    • numbers → int, long, double
    • booleans, strings
    • DateTime, Guid when possible
    • otherwise a raw JSON element

FeatureCollection (DHI extension)

{
  "type": "FeatureCollection",
  "features": [ { ...Feature... }, { ... } ],
  "attributes": [
    { "name": "Id", "Type": "System.Int32", "length": 4, "displayName": "Identifier" },
    { "name": "Value", "Type": "System.Double", "length": 8 }
  ]
}
  • Standard type: FeatureCollection + features.
  • DHI extension: attributes list with schema (IAttribute) for consumers.

BoundingBox

{
  "xmin": 12.3,
  "xmax": 13.7,
  "ymin": 55.1,
  "ymax": 56.9
}

CoordinateReferenceSystem (CRS)

{
  "type": "name",
  "properties": { "name": "EPSG:3857" }
}

Associations & FeatureInfo (DHI types)

FeatureInfo (internal container for Associations + AttributeValues) uses:

{
  "Associations": [
    { "Id": 42, "Type": "My.Namespace.MyAssociationType" }
  ],
  "AttributeValues": {
    "something": 123,
    "when": "2025-01-01T00:00:00Z"
  }
}

Converter reference

GeometryConverter (↔ IGeometry)

  • Read: builds the appropriate DHI.Spatial geometry by inspecting type and coordinates. If crs exists, it’s deserialized with CoordinateReferenceSystemConverter.
  • Write: emits type, coordinates (arrays via PositionConverter), and crs.
  • Errors: unsupported type throws NotSupportedException.

GeometryCollectionConverter (↔ IGeometryCollection)

  • Writes: { "type": "GeometryCollection", "geometries": [ ... ] }.

FeatureConverter (↔ IFeature)

  • Writes: { "type": "Feature", "geometry": {...}, "properties": {...} }.

FeatureCollectionConverter (↔ IFeatureCollection)

  • Writes: { "type": "FeatureCollection", "features": [...], "attributes": [...] }.

FeatureInfoConverter (↔ FeatureInfo)

  • Handles:
    • Associations: list of IAssociation (see AssociationConverter)
    • AttributeValues: dictionary with inferred types

AttributeConverter (↔ IAttribute)

  • Read: Accepts either "Type": "System.Double" or friendly strings like "double", "int", "datetime", etc.
  • Write: "name", "Type" (full type name), "length", and optionally "displayName" and "defaultValue".
    • Note: the current implementation only writes displayName when blank, not when populated. If you need it always, adjust the writer.

AssociationConverter (↔ IAssociation)

  • Write: { "Id": <value>, "Type": "<type-string>" }. Type string comes from TypeStringConverter (FullName by default, or friendly format).
  • Read: Tries to Type.GetType(string) and reconstruct Association<T> dynamically.
    • If Type is missing, falls back to generic type argument where possible.
    • If the CLR type cannot be resolved from string, you’ll get null or an exception depending on usage.

BoundingBoxConverter (↔ BoundingBox)

  • Emits { xmin, xmax, ymin, ymax } using DoubleConverter for numeric formatting.

CoordinateReferenceSystemConverter (↔ CoordinateReferenceSystem)

  • Minimal CRS object: { "type": "...", "properties": { ... } }.

PositionConverter (↔ Position)

  • Read: [x, y] or [x, y, z].
  • Write: [x, y] (+ optional z), with DoubleConverter formatting.

ObjectToInferredTypeConverter

  • Deserializes object properties into “best-match” .NET types (bool, numbers, DateTime, Guid, string), otherwise returns a raw JsonElement.

DictionaryConverter<TValue>

  • Generic converter for IDictionary<TKey, TValue>. Used widely for Properties, AttributeValues, etc.
  • Read: values parsed using ObjectToInferredTypeConverter.
  • Write:
    • Primitives/arrays are written as-is.
    • Objects: empty objects ({}) become null. Non-empty objects get inlined by enumerating their properties.
      • Heads-up: Inline behavior is only safe if you don’t rely on the dictionary key wrapping that object. Avoid putting complex objects as dictionary values unless you know this flattening is desired.

DoubleConverter

  • Writes doubles without scientific notation and ensures integers are emitted with a decimal (e.g., 1.0).
  • Uses Utf8JsonWriter.WriteRawValue(...) with input validation disabled for performance — assumes well-formed numeric values.

TypeStringConverter

  • Write: writes Type.FullName by default, or a friendly assembly-qualified-ish form if constructed with serializedFriendlyName: true.
  • Read: Type.GetType(string); requires the type to be loadable in the current AppDomain.

Usage examples

Serialize a FeatureCollection

var poly = new Polygon();
poly.Coordinates.Add(new List<Position> {
    new(0,0), new(10,0), new(10,5), new(0,5), new(0,0)
});
poly.CRS = new CoordinateReferenceSystem { Type = "name" };
poly.CRS.Properties["name"] = "EPSG:4326";

var feature = new Feature(poly);
feature.AttributeValues["name"] = "Area A";
feature.AttributeValues["elevation"] = 12.5;

var fc = new FeatureCollection();
fc.Features.Add(feature);
fc.Attributes.Add(new DHI.Spatial.Attribute("elevation", typeof(double), 8, "Elevation (m)"));

var json = JsonSerializer.Serialize<IFeatureCollection>(fc, jsonOptions);

Deserialize back

var deserialized = JsonSerializer.Deserialize<IFeatureCollection>(json, jsonOptions);
foreach (var f in deserialized.Features)
{
    var geom = f.Geometry; // IGeometry
    var props = f.AttributeValues; // IDictionary<string, object?>
}

Interop & edge cases

  • Geometry support: only the 6 canonical GeoJSON geometry types + GeometryCollection.
  • Coordinates: always [x, y] (with optional z on write if set). We do not emit measures or SRIDs.
  • CRS: we serialize a simple object (type + properties), not the deprecated crs member from the GeoJSON RFC. Consumers must agree on this minimal shape.
  • Numbers: DoubleConverter forces non-scientific output and “.0” suffix for integer-looking doubles. Great for strict parsers; keep it if downstream needs stable formatting.
  • Attribute Type strings: writer uses FullName. The reader also accepts common aliases ("int", "double", "datetime", etc.).
  • Associations: resolving Type during read relies on Type.GetType(). If the assembly isn’t loaded or the string isn’t assembly-qualified enough, type resolution may fail. You can switch to the “friendly” format via new TypeStringConverter(serializedFriendlyName:true) and ensure both sides share the same assembly resolution.
  • Dictionary values that are objects: the DictionaryConverter flattens non-empty object values (doesn’t nest them under the dictionary key). Avoid complex object values unless you want that effect.

Performance notes

  • Converters are lightweight and stream-friendly.
  • We create small ad-hoc JsonSerializerOptions instances inside a few converters to apply specialized converters (e.g., inferred types). They are short-lived; if profiling shows pressure, you can pool options or refactor to reuse shared instances.