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,crsobject shape, type metadata). Property name casing follows thePropertyNamingPolicyset on your JsonSerializerOptions — configureJsonNamingPolicy.CamelCaseif strict RFC 7946 lowercase keys are required.
TL;DR — What you get¶
- Serialize/deserialize:
IGeometry(Point, LineString, Polygon, Multi*, GeometryCollection)IFeatureandIFeatureCollectionBoundingBox,CoordinateReferenceSystem,FeatureInfoIAttributeandIAssociation
- Robust numeric formatting for coordinates (no scientific notation;
1emitted as1.0). - “Smart”
objectvalue 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
typevalues:Point,MultiPoint,LineString,MultiLineString,Polygon,MultiPolygon. - Coordinates are arrays of
[x, y]or[x, y, z](zonly written whenPosition.Zis 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"
}
}
propertiesis aDictionary<string, object?>. Values are inferred at runtime:- numbers →
int,long,double - booleans, strings
DateTime,Guidwhen possible- otherwise a raw JSON element
- numbers →
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:
attributeslist 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.Spatialgeometry by inspectingtypeandcoordinates. Ifcrsexists, it’s deserialized withCoordinateReferenceSystemConverter. - Write: emits
type,coordinates(arrays viaPositionConverter), andcrs. - 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 ofIAssociation(seeAssociationConverter)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
displayNamewhen blank, not when populated. If you need it always, adjust the writer.
- Note: the current implementation only writes
AssociationConverter (↔ IAssociation)¶
- Write:
{ "Id": <value>, "Type": "<type-string>" }. Type string comes fromTypeStringConverter(FullNameby default, or friendly format). - Read: Tries to
Type.GetType(string)and reconstructAssociation<T>dynamically.- If
Typeis missing, falls back to generic type argument where possible. - If the CLR type cannot be resolved from string, you’ll get
nullor an exception depending on usage.
- If
BoundingBoxConverter (↔ BoundingBox)¶
- Emits
{ xmin, xmax, ymin, ymax }usingDoubleConverterfor numeric formatting.
CoordinateReferenceSystemConverter (↔ CoordinateReferenceSystem)¶
- Minimal CRS object:
{ "type": "...", "properties": { ... } }.
PositionConverter (↔ Position)¶
Read:[x, y]or[x, y, z].Write:[x, y](+ optionalz), withDoubleConverterformatting.
ObjectToInferredTypeConverter¶
- Deserializes
objectproperties into “best-match” .NET types (bool, numbers, DateTime, Guid, string), otherwise returns a rawJsonElement.
DictionaryConverter<TValue>¶
- Generic converter for
IDictionary<TKey, TValue>. Used widely forProperties,AttributeValues, etc. - Read: values parsed using
ObjectToInferredTypeConverter. - Write:
- Primitives/arrays are written as-is.
- Objects: empty objects (
{}) becomenull. 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.FullNameby default, or a friendly assembly-qualified-ish form if constructed withserializedFriendlyName: 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 optionalzon write if set). We do not emit measures or SRIDs. - CRS: we serialize a simple object (
type+properties), not the deprecatedcrsmember from the GeoJSON RFC. Consumers must agree on this minimal shape. - Numbers:
DoubleConverterforces non-scientific output and “.0” suffix for integer-looking doubles. Great for strict parsers; keep it if downstream needs stable formatting. - Attribute
Typestrings: writer usesFullName. The reader also accepts common aliases ("int","double","datetime", etc.). - Associations: resolving
Typeduring read relies onType.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 vianew TypeStringConverter(serializedFriendlyName:true)and ensure both sides share the same assembly resolution. - Dictionary values that are objects: the
DictionaryConverterflattens 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
JsonSerializerOptionsinstances 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.