diff --git a/Microsoft.Azure.Cosmos/src/Spatial/BoundingBox.cs b/Microsoft.Azure.Cosmos/src/Spatial/BoundingBox.cs
index b80232a691..2219811f4c 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/BoundingBox.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/BoundingBox.cs
@@ -7,6 +7,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System;
using System.Runtime.Serialization;
using Microsoft.Azure.Cosmos.Spatial.Converters;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -14,6 +15,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
[DataContract]
[JsonConverter(typeof(BoundingBoxJsonConverter))]
+ [System.Text.Json.Serialization.JsonConverter(typeof(BoundingBoxSTJConverter))]
public sealed class BoundingBox : IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/CrsJsonConverter.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/CrsJsonConverter.cs
index 5b1879532b..9ec73e7654 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/Converters/CrsJsonConverter.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/CrsJsonConverter.cs
@@ -122,7 +122,7 @@ public override object ReadJson(
throw new JsonSerializationException(RMResources.SpatialFailedToDeserializeCrs);
}
- return new LinkedCrs(crsHref.Value(), crsHrefType.Value());
+ return new LinkedCrs(crsHref.Value(), crsHrefType?.Value());
default:
throw new JsonSerializationException(RMResources.SpatialFailedToDeserializeCrs);
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/BoundingBoxSTJConverter.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/BoundingBoxSTJConverter.cs
new file mode 100644
index 0000000000..3413b08b7c
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/BoundingBoxSTJConverter.cs
@@ -0,0 +1,64 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters
+{
+ using System;
+ using System.Globalization;
+ using System.Linq;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using Microsoft.Azure.Documents;
+ ///
+ /// Converter used to support System.Text.Json serialization/deserialization of BoundingBox.
+ ///
+ internal sealed class BoundingBoxSTJConverter : JsonConverter
+ {
+ ///
+ /// Deserializes a BoundingBox from a JSON array of coordinates.
+ /// The array contains min coordinates followed by max coordinates.
+ /// Example: [minLon, minLat, maxLon, maxLat] for 2D or [minLon, minLat, minAlt, maxLon, maxLat, maxAlt] for 3D.
+ ///
+ public override BoundingBox Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ double[] coordinates = JsonSerializer.Deserialize(ref reader, options);
+ if (coordinates == null)
+ {
+ return null;
+ }
+
+ if (coordinates.Length % 2 != 0 || coordinates.Length < 4)
+ {
+ throw new JsonException(RMResources.SpatialBoundingBoxInvalidCoordinates);
+ }
+
+ return new BoundingBox(
+ new Position(coordinates.Take(coordinates.Length / 2).ToList()),
+ new Position(coordinates.Skip(coordinates.Length / 2).ToList()));
+ }
+
+ ///
+ /// Serializes a BoundingBox to a JSON array.
+ /// Outputs min coordinates followed by max coordinates.
+ ///
+ public override void Write(Utf8JsonWriter writer, BoundingBox box, JsonSerializerOptions options)
+ {
+ writer.WriteStartArray();
+ foreach (double coordinate in box.Min.Coordinates.Concat(box.Max.Coordinates))
+ {
+ // Check if the number is effectively an integer.
+ if (coordinate == Math.Truncate(coordinate))
+ {
+ // If so, write it with one decimal place to match Newtonsoft's [x.0] format.
+ writer.WriteRawValue(coordinate.ToString("0.0", CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ writer.WriteRawValue(coordinate.ToString("R", CultureInfo.InvariantCulture));
+ }
+ }
+ writer.WriteEndArray();
+ }
+ }
+}
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/CrsSTJConverter.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/CrsSTJConverter.cs
new file mode 100644
index 0000000000..62f4411672
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/CrsSTJConverter.cs
@@ -0,0 +1,128 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters
+{
+ using System;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using Microsoft.Azure.Documents;
+ ///
+ /// Converter used to support System.Text.Json serialization/deserialization of Crs (Coordinate Reference System).
+ /// Handles NamedCrs, LinkedCrs, and Unspecified CRS types.
+ /// Ensures output format matches Newtonsoft.Json exactly.
+ ///
+ internal sealed class CrsSTJConverter : JsonConverter
+ {
+ public override bool HandleNull => true;
+
+ ///
+ /// Deserializes a CRS from JSON.
+ /// Supports named CRS (e.g., EPSG:4326) and linked CRS with optional type.
+ /// Returns Crs.Unspecified for null values.
+ ///
+ public override Crs Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return Crs.Unspecified;
+ }
+
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException(RMResources.JsonUnexpectedToken);
+ }
+
+ JsonElement rootElement = JsonDocument.ParseValue(ref reader).RootElement;
+ if (!rootElement.TryGetProperty(STJMetaDataFields.Properties, out JsonElement properties) || (properties.ValueKind != JsonValueKind.Object))
+ {
+ throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
+ }
+
+ if (!rootElement.TryGetProperty(STJMetaDataFields.Type, out JsonElement crsType) || crsType.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
+ }
+
+ switch (crsType.GetString())
+ {
+ case "name":
+ if (!properties.TryGetProperty(STJMetaDataFields.Name, out JsonElement crsName) || crsName.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
+ }
+ return new NamedCrs(crsName.GetString());
+
+ case "link":
+ if (!properties.TryGetProperty(STJMetaDataFields.Href, out JsonElement crsHref) || crsHref.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
+ }
+
+ if (properties.TryGetProperty(STJMetaDataFields.Type, out JsonElement crsHrefType))
+ {
+ if (crsHrefType.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
+ }
+ return new LinkedCrs(crsHref.GetString(), crsHrefType.GetString());
+ }
+ return new LinkedCrs(crsHref.GetString());
+
+ default:
+ throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
+ }
+
+ }
+ ///
+ /// Serializes a CRS to JSON.
+ /// Outputs different JSON structures based on CRS type (Named, Linked, or Unspecified).
+ ///
+ public override void Write(Utf8JsonWriter writer, Crs crs, JsonSerializerOptions options)
+ {
+ if (crs == null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ switch (crs.Type)
+ {
+ case CrsType.Linked:
+ {
+ writer.WriteStartObject();
+ LinkedCrs linkedCrs = (LinkedCrs)crs;
+ writer.WriteString(STJMetaDataFields.Type, "link");
+ writer.WritePropertyName(STJMetaDataFields.Properties);
+ writer.WriteStartObject();
+ writer.WriteString(STJMetaDataFields.Href, linkedCrs.Href);
+ if (linkedCrs.HrefType != null)
+ {
+ writer.WriteString(STJMetaDataFields.Type, linkedCrs.HrefType);
+ }
+
+ writer.WriteEndObject();
+ writer.WriteEndObject();
+ break;
+ }
+ case CrsType.Named:
+ {
+ writer.WriteStartObject();
+ NamedCrs namedCrs = (NamedCrs)crs;
+ writer.WriteString(STJMetaDataFields.Type, "name");
+ writer.WritePropertyName(STJMetaDataFields.Properties);
+ writer.WriteStartObject();
+ writer.WriteString(STJMetaDataFields.Name, namedCrs.Name);
+ writer.WriteEndObject();
+ writer.WriteEndObject();
+ break;
+ }
+ case CrsType.Unspecified:
+ writer.WriteNullValue();
+ break;
+ }
+
+ }
+ }
+}
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/GeometrySTJConverter.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/GeometrySTJConverter.cs
new file mode 100644
index 0000000000..13254b1af1
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/GeometrySTJConverter.cs
@@ -0,0 +1,217 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using Microsoft.Azure.Documents;
+
+ ///
+ /// A factory converter for all Geometry types including Point, LineString, Polygon,
+ /// MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection.
+ ///
+ internal sealed class GeometrySTJConverter : JsonConverter
+ {
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeof(Geometry).IsAssignableFrom(typeToConvert);
+ }
+
+ public override Geometry Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ JsonElement rootElement = JsonDocument.ParseValue(ref reader).RootElement;
+ (string typeName, GeometryParams geometryParams) = this.ReadGeometryProperties(rootElement, options);
+ return this.CreateGeometry(typeName, geometryParams, rootElement, options);
+ }
+
+ public override void Write(Utf8JsonWriter writer, Geometry value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+
+ writer.WriteString(STJMetaDataFields.Type, value.Type.ToString());
+ this.WriteCoordinates(writer, value, options);
+ this.WriteOptionalProperties(writer, value, options);
+
+ writer.WriteEndObject();
+ }
+
+ ///
+ /// Writes the coordinates or geometries property based on the geometry type.
+ /// GeometryCollection uses 'geometries' property instead of 'coordinates'.
+ ///
+ private void WriteCoordinates(Utf8JsonWriter writer, Geometry value, JsonSerializerOptions options)
+ {
+ switch (value.Type)
+ {
+ case GeometryType.Point:
+ writer.WritePropertyName(STJMetaDataFields.Coordinates);
+ JsonSerializer.Serialize(writer, ((Point)value).Position, options);
+ break;
+ case GeometryType.LineString:
+ writer.WritePropertyName(STJMetaDataFields.Coordinates);
+ JsonSerializer.Serialize(writer, ((LineString)value).Positions, options);
+ break;
+ case GeometryType.Polygon:
+ writer.WritePropertyName(STJMetaDataFields.Coordinates);
+ JsonSerializer.Serialize(writer, ((Polygon)value).Rings, options);
+ break;
+ case GeometryType.MultiPoint:
+ writer.WritePropertyName(STJMetaDataFields.Coordinates);
+ JsonSerializer.Serialize(writer, ((MultiPoint)value).Points, options);
+ break;
+ case GeometryType.MultiLineString:
+ writer.WritePropertyName(STJMetaDataFields.Coordinates);
+ JsonSerializer.Serialize(writer, ((MultiLineString)value).LineStrings, options);
+ break;
+ case GeometryType.MultiPolygon:
+ writer.WritePropertyName(STJMetaDataFields.Coordinates);
+ JsonSerializer.Serialize(writer, ((MultiPolygon)value).Polygons, options);
+ break;
+ case GeometryType.GeometryCollection:
+ writer.WritePropertyName(STJMetaDataFields.Geometries);
+ JsonSerializer.Serialize(writer, ((GeometryCollection)value).Geometries, options);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value.Type), "Unsupported geometry type.");
+ }
+ }
+
+ private void WriteOptionalProperties(Utf8JsonWriter writer, Geometry value, JsonSerializerOptions options)
+ {
+ // Match Newtonsoft Order property: Crs (2) then BoundingBox (3)
+ if (value.Crs != null && !value.Crs.Equals(Crs.Default))
+ {
+ writer.WritePropertyName(STJMetaDataFields.Crs);
+ JsonSerializer.Serialize(writer, value.Crs, options);
+ }
+
+ if (value.BoundingBox != null)
+ {
+ writer.WritePropertyName(STJMetaDataFields.Bbox);
+ JsonSerializer.Serialize(writer, value.BoundingBox, options);
+ }
+
+ if (value.AdditionalProperties != null)
+ {
+ foreach (KeyValuePair property in value.AdditionalProperties)
+ {
+ writer.WritePropertyName(property.Key);
+ JsonSerializer.Serialize(writer, property.Value, options);
+ }
+ }
+ }
+
+ private (string typeName, GeometryParams geometryParams) ReadGeometryProperties(JsonElement rootElement, JsonSerializerOptions options)
+ {
+ if (!rootElement.TryGetProperty(STJMetaDataFields.Type, out JsonElement typeElement) || typeElement.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException(RMResources.SpatialInvalidGeometryType);
+ }
+
+ string typeName = typeElement.GetString();
+
+ BoundingBox boundingBox = null;
+ if (rootElement.TryGetProperty(STJMetaDataFields.Bbox, out JsonElement bboxElement))
+ {
+ boundingBox = bboxElement.Deserialize(options);
+ }
+
+ Crs crs = null;
+ if (rootElement.TryGetProperty(STJMetaDataFields.Crs, out JsonElement crsElement))
+ {
+ crs = crsElement.Deserialize(options);
+ }
+
+ if (crs != null && crs.Equals(Crs.Unspecified))
+ {
+ crs = null;
+ }
+
+ IDictionary additionalProperties = null;
+ foreach (JsonProperty property in rootElement.EnumerateObject())
+ {
+ if (property.Name != STJMetaDataFields.Type && property.Name != STJMetaDataFields.Coordinates && property.Name != STJMetaDataFields.Bbox && property.Name != STJMetaDataFields.Crs && property.Name != STJMetaDataFields.Geometries)
+ {
+ additionalProperties ??= new Dictionary();
+ additionalProperties[property.Name] = this.ReadValue(property.Value);
+ }
+ }
+
+ return (typeName, new GeometryParams { BoundingBox = boundingBox, Crs = crs, AdditionalProperties = additionalProperties });
+ }
+
+ ///
+ /// Creates the appropriate Geometry subclass based on the type name.
+ /// Supports all geometry types including GeometryCollection which contains nested geometries.
+ ///
+ private Geometry CreateGeometry(string typeName, GeometryParams geometryParams, JsonElement rootElement, JsonSerializerOptions options)
+ {
+ switch (typeName)
+ {
+ case "Point":
+ Position pointCoordinates = rootElement.GetProperty(STJMetaDataFields.Coordinates).Deserialize(options);
+ return new Point(pointCoordinates, geometryParams);
+ case "MultiPoint":
+ List multiPointCoordinates = rootElement.GetProperty(STJMetaDataFields.Coordinates).Deserialize>(options);
+ return new MultiPoint(multiPointCoordinates, geometryParams);
+ case "LineString":
+ List lineStringCoordinates = rootElement.GetProperty(STJMetaDataFields.Coordinates).Deserialize>(options);
+ return new LineString(lineStringCoordinates, geometryParams);
+ case "MultiLineString":
+ List multiLineStringCoordinates = rootElement.GetProperty(STJMetaDataFields.Coordinates).Deserialize>(options);
+ return new MultiLineString(multiLineStringCoordinates, geometryParams);
+ case "Polygon":
+ List polygonCoordinates = rootElement.GetProperty(STJMetaDataFields.Coordinates).Deserialize>(options);
+ return new Polygon(polygonCoordinates, geometryParams);
+ case "MultiPolygon":
+ List multiPolygonCoordinates = rootElement.GetProperty(STJMetaDataFields.Coordinates).Deserialize>(options);
+ return new MultiPolygon(multiPolygonCoordinates, geometryParams);
+ case "GeometryCollection":
+ List geometries = rootElement.GetProperty(STJMetaDataFields.Geometries).Deserialize>(options);
+ return new GeometryCollection(geometries, geometryParams);
+ default:
+ throw new JsonException(RMResources.SpatialInvalidGeometryType);
+ }
+ }
+
+ ///
+ /// Reads a JSON value and converts it to the appropriate .NET type.
+ /// Handles primitive types, objects, and arrays for additional properties.
+ /// Matches Newtonsoft.Json behavior by returning Int32 for numbers that fit in 32 bits.
+ ///
+ private object ReadValue(JsonElement element)
+ {
+ switch (element.ValueKind)
+ {
+ case JsonValueKind.True:
+ return true;
+ case JsonValueKind.False:
+ return false;
+ case JsonValueKind.String:
+ return element.GetString();
+ case JsonValueKind.Number:
+ // Match Newtonsoft behavior: try Int32 first, then Int64, then Double
+ if (element.TryGetInt32(out int i))
+ {
+ return i;
+ }
+ if (element.TryGetInt64(out long l))
+ {
+ return l;
+ }
+ return element.GetDouble();
+ case JsonValueKind.Null:
+ return null;
+ case JsonValueKind.Object:
+ case JsonValueKind.Array:
+ default:
+ return element.Clone();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/LineStringCoordinatesSTJConverter.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/LineStringCoordinatesSTJConverter.cs
new file mode 100644
index 0000000000..c7eb50590d
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/LineStringCoordinatesSTJConverter.cs
@@ -0,0 +1,49 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial;
+ using Microsoft.Azure.Documents;
+ ///
+ /// Converter used to support System.Text.Json serialization/deserialization of LineStringCoordinates.
+ /// LineStringCoordinates represents an array of positions forming a line string.
+ /// Used within MultiLineString geometries.
+ ///
+ internal sealed class LineStringCoordinatesSTJConverter : JsonConverter
+ {
+ ///
+ ///
+ /// Deserializes LineStringCoordinates from a JSON array of Position objects.
+ ///
+ public override LineStringCoordinates Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ // A LineStringCoordinate is just an array of positions.
+ // System.Text.Json cannot deserialize into a ReadOnlyCollection, so we deserialize into a List first.
+ List positions = JsonSerializer.Deserialize>(ref reader, options);
+ return new LineStringCoordinates(positions);
+ }
+
+ ///
+ ///
+ /// Serializes LineStringCoordinates to a JSON array of positions.
+ ///
+ public override void Write(
+ Utf8JsonWriter writer,
+ LineStringCoordinates value,
+ JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value.Positions, options);
+ }
+
+ }
+
+}
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/LinearRingSTJConverter.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/LinearRingSTJConverter.cs
new file mode 100644
index 0000000000..669ed17b39
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/LinearRingSTJConverter.cs
@@ -0,0 +1,44 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial;
+ using Microsoft.Azure.Documents;
+ ///
+ /// Converter used to support System.Text.Json serialization/deserialization of LinearRing.
+ /// A LinearRing is a closed LineString (first and last positions must be identical).
+ ///
+ internal sealed class LinearRingSTJConverter : JsonConverter
+ {
+ ///
+ /// Deserializes a LinearRing from a JSON array of Position objects.
+ /// The ring must be closed (first position equals last position).
+ ///
+ public override LinearRing Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.StartArray)
+ {
+ throw new JsonException(RMResources.JsonUnexpectedToken);
+ }
+
+ IList positions = JsonSerializer.Deserialize>(ref reader, options);
+ return new LinearRing(positions);
+ }
+
+ ///
+ /// Serializes a LinearRing to a JSON array of positions.
+ ///
+ public override void Write(Utf8JsonWriter writer, LinearRing linearRing, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, linearRing.Positions, options);
+ }
+
+ }
+
+}
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/PolygonCoordinatesSTJConverter.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/PolygonCoordinatesSTJConverter.cs
new file mode 100644
index 0000000000..66a14d2ff2
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/PolygonCoordinatesSTJConverter.cs
@@ -0,0 +1,50 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial;
+ using Microsoft.Azure.Documents;
+ ///
+ /// Converter used to support System.Text.Json serialization/deserialization of PolygonCoordinates.
+ /// PolygonCoordinates represents an array of LinearRings forming a polygon.
+ /// The first ring is the exterior boundary, subsequent rings are holes.
+ /// Used within MultiPolygon geometries.
+ ///
+ internal sealed class PolygonCoordinatesSTJConverter : JsonConverter
+ {
+ ///
+ ///
+ /// Deserializes PolygonCoordinates from a JSON array of LinearRing objects.
+ ///
+ public override PolygonCoordinates Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ // A PolygonCoordinate is just an array of LinearRings.
+ // System.Text.Json cannot deserialize into a ReadOnlyCollection, so we deserialize into a List first.
+ List rings = JsonSerializer.Deserialize>(ref reader, options);
+ return new PolygonCoordinates(rings);
+ }
+
+ ///
+ ///
+ /// Serializes PolygonCoordinates to a JSON array of LinearRings.
+ ///
+ public override void Write(
+ Utf8JsonWriter writer,
+ PolygonCoordinates value,
+ JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value.Rings, options);
+ }
+
+ }
+
+}
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/PositionSTJConverter.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/PositionSTJConverter.cs
new file mode 100644
index 0000000000..c1de2d42b1
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/PositionSTJConverter.cs
@@ -0,0 +1,65 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Globalization;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using Microsoft.Azure.Documents;
+ ///
+ /// Converter used to support System.Text.Json serialization/deserialization of Position.
+ /// Handles 2D positions (longitude, latitude) and 3D positions (longitude, latitude, altitude).
+ /// Ensures output format matches Newtonsoft.Json exactly.
+ ///
+ internal sealed class PositionSTJConverter : JsonConverter
+ {
+ ///
+ /// Deserializes a Position from a JSON array of coordinates.
+ /// Requires at least 2 coordinates (longitude, latitude). Altitude is optional.
+ ///
+ public override Position Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.StartArray)
+ {
+ throw new JsonException(RMResources.JsonUnexpectedToken);
+ }
+
+ IList coordinates = JsonSerializer.Deserialize>(ref reader, options);
+ if (coordinates == null || coordinates.Count < 2)
+ {
+ throw new JsonException(RMResources.SpatialInvalidPosition);
+ }
+
+ return new Position(coordinates);
+ }
+ ///
+ /// Serializes a Position to a JSON array.
+ /// Integer coordinates are formatted with .0 decimal to match Newtonsoft.Json output.
+ /// Uses "R" format for non-integer values to preserve full precision.
+ ///
+ public override void Write(Utf8JsonWriter writer, Position position, JsonSerializerOptions options)
+ {
+ writer.WriteStartArray();
+ foreach (double coordinate in position.Coordinates)
+ {
+ // Check if the number is effectively an integer.
+ if (coordinate == Math.Truncate(coordinate))
+ {
+ // If so, write it with one decimal place to match Newtonsoft's [x.0] format.
+ writer.WriteRawValue(coordinate.ToString("0.0", CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ writer.WriteRawValue(coordinate.ToString("R", CultureInfo.InvariantCulture));
+ }
+ }
+ writer.WriteEndArray();
+ }
+
+ }
+}
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/STJMetaDataFields.cs b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/STJMetaDataFields.cs
new file mode 100644
index 0000000000..ba41f769cc
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Converters/STJConverters/STJMetaDataFields.cs
@@ -0,0 +1,128 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ ///
+ /// Constants for JSON property names used in spatial geometry serialization.
+ /// These field names are part of the GeoJSON specification and must match exactly.
+ ///
+ internal static class STJMetaDataFields
+ {
+ ///
+ /// Position property name.
+ ///
+ public const string Position = "position";
+
+ ///
+ /// Additional properties field name.
+ ///
+ public const string AdditionalProperties = "additionalProperties";
+
+ ///
+ /// Coordinate Reference System property name.
+ ///
+ public const string Crs = "crs";
+
+ ///
+ /// Bounding box property name.
+ ///
+ public const string BoundingBox = "boundingBox";
+
+ ///
+ /// Geometry type property name.
+ ///
+ public const string Type = "type";
+
+ ///
+ /// Points array property name (used in MultiPoint).
+ ///
+ public const string Points = "points";
+
+ ///
+ /// Positions array property name (used in LineString).
+ ///
+ public const string Positions = "positions";
+
+ ///
+ /// LineStrings array property name (used in MultiLineString).
+ ///
+ public const string LineStrings = "lineStrings";
+
+ ///
+ /// Rings array property name (used in Polygon).
+ ///
+ public const string Rings = "rings";
+
+ ///
+ /// Polygons array property name (used in MultiPolygon).
+ ///
+ public const string Polygons = "polygons";
+
+ ///
+ /// Maximum coordinates in bounding box.
+ ///
+ public const string Max = "max";
+
+ ///
+ /// Minimum coordinates in bounding box.
+ ///
+ public const string Min = "min";
+
+ ///
+ /// CRS properties object.
+ ///
+ public const string Properties = "properties";
+
+ ///
+ /// Name property (used in NamedCrs).
+ ///
+ public const string Name = "name";
+
+ ///
+ /// Link type property (used in LinkedCrs).
+ ///
+ public const string Link = "link";
+
+ ///
+ /// Href property (used in LinkedCrs).
+ ///
+ public const string Href = "href";
+
+ ///
+ /// Geometries array property name (used in GeometryCollection).
+ ///
+ public const string Geometries = "geometries";
+
+ ///
+ /// Longitude coordinate.
+ ///
+ public const string Longitude = "longitude";
+
+ ///
+ /// Altitude coordinate.
+ ///
+ public const string Altitude = "altitude";
+
+ ///
+ /// Latitude coordinate.
+ ///
+ public const string Latitude = "latitude";
+
+ ///
+ /// Coordinates array property name.
+ ///
+ public const string Coordinates = "coordinates";
+
+ ///
+ /// Bounding box property name in GeoJSON format.
+ ///
+ public const string Bbox = "bbox";
+
+ }
+}
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Crs.cs b/Microsoft.Azure.Cosmos/src/Spatial/Crs.cs
index 94427b701b..5b31fbf628 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/Crs.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Crs.cs
@@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
{
using System.Runtime.Serialization;
using Microsoft.Azure.Cosmos.Spatial.Converters;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -13,6 +14,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
[DataContract]
[JsonConverter(typeof(CrsJsonConverter))]
+ [System.Text.Json.Serialization.JsonConverter(typeof(CrsSTJConverter))]
public abstract class Crs
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Geometry.cs b/Microsoft.Azure.Cosmos/src/Spatial/Geometry.cs
index e3ab8e2768..b6431c6415 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/Geometry.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Geometry.cs
@@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Linq;
using System.Runtime.Serialization;
using Microsoft.Azure.Cosmos.Spatial.Converters;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Microsoft.Azure.Documents;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@@ -19,6 +20,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
[DataContract]
[JsonObject(MemberSerialization.OptIn)]
[JsonConverter(typeof(GeometryJsonConverter))]
+ [System.Text.Json.Serialization.JsonConverter(typeof(GeometrySTJConverter))]
public abstract class Geometry
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/GeometryCollection.cs b/Microsoft.Azure.Cosmos/src/Spatial/GeometryCollection.cs
index f14a753f05..516aa2f486 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/GeometryCollection.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/GeometryCollection.cs
@@ -9,12 +9,14 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
/// Represents a geometry consisting of other geometries.
///
[DataContract]
+ [System.Text.Json.Serialization.JsonConverter(typeof(GeometrySTJConverter))]
internal sealed class GeometryCollection : Geometry, IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/LineString.cs b/Microsoft.Azure.Cosmos/src/Spatial/LineString.cs
index 00b2c077ac..72ed73319e 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/LineString.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/LineString.cs
@@ -9,12 +9,14 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
/// Represents a geometry consisting of connected line segments.
///
[DataContract]
+ [System.Text.Json.Serialization.JsonConverter(typeof(GeometrySTJConverter))]
public sealed class LineString : Geometry, IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/LineStringCoordinates.cs b/Microsoft.Azure.Cosmos/src/Spatial/LineStringCoordinates.cs
index e231d78f5f..0e9258500f 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/LineStringCoordinates.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/LineStringCoordinates.cs
@@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Linq;
using System.Runtime.Serialization;
using Converters;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -18,6 +19,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
[DataContract]
[JsonConverter(typeof(LineStringCoordinatesJsonConverter))]
+ [System.Text.Json.Serialization.JsonConverter(typeof(LineStringCoordinatesSTJConverter))]
internal sealed class LineStringCoordinates : IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/LinearRing.cs b/Microsoft.Azure.Cosmos/src/Spatial/LinearRing.cs
index 9437d9e623..5fc60a1112 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/LinearRing.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/LinearRing.cs
@@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Linq;
using System.Runtime.Serialization;
using Microsoft.Azure.Cosmos.Spatial.Converters;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -20,6 +21,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
[DataContract]
[JsonConverter(typeof(LinearRingJsonConverter))]
+ [System.Text.Json.Serialization.JsonConverter(typeof(LinearRingSTJConverter))]
public sealed class LinearRing : IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/MultiLineString.cs b/Microsoft.Azure.Cosmos/src/Spatial/MultiLineString.cs
index 0ad888187b..27ab1ee578 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/MultiLineString.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/MultiLineString.cs
@@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -16,6 +17,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
/// .
[DataContract]
+ [System.Text.Json.Serialization.JsonConverter(typeof(GeometrySTJConverter))]
internal sealed class MultiLineString : Geometry, IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/MultiPoint.cs b/Microsoft.Azure.Cosmos/src/Spatial/MultiPoint.cs
index c4ed9e68c9..3c1971788c 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/MultiPoint.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/MultiPoint.cs
@@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -16,6 +17,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
/// .
[DataContract]
+ [System.Text.Json.Serialization.JsonConverter(typeof(GeometrySTJConverter))]
internal sealed class MultiPoint : Geometry, IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/MultiPolygon.cs b/Microsoft.Azure.Cosmos/src/Spatial/MultiPolygon.cs
index 83646dd1cd..3cc7333557 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/MultiPolygon.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/MultiPolygon.cs
@@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -16,6 +17,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
///
[DataContract]
+ [System.Text.Json.Serialization.JsonConverter(typeof(GeometrySTJConverter))]
public sealed class MultiPolygon : Geometry, IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Point.cs b/Microsoft.Azure.Cosmos/src/Spatial/Point.cs
index 640710f0c0..1d948d96c9 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/Point.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Point.cs
@@ -6,12 +6,14 @@ namespace Microsoft.Azure.Cosmos.Spatial
{
using System;
using System.Runtime.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
/// Point geometry class in the Azure Cosmos DB service.
///
[DataContract]
+ [System.Text.Json.Serialization.JsonConverter(typeof(GeometrySTJConverter))]
public sealed class Point : Geometry, IEquatable
{
///
@@ -57,6 +59,7 @@ public Point(Position position, GeometryParams geometryParams)
}
this.Position = position;
+
}
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Polygon.cs b/Microsoft.Azure.Cosmos/src/Spatial/Polygon.cs
index be22052cfa..1c67cd4f12 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/Polygon.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Polygon.cs
@@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -58,6 +59,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
///
[DataContract]
+ [System.Text.Json.Serialization.JsonConverter(typeof(GeometrySTJConverter))]
public sealed class Polygon : Geometry, IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/PolygonCoordinates.cs b/Microsoft.Azure.Cosmos/src/Spatial/PolygonCoordinates.cs
index 5c7f3b65eb..3eced645bf 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/PolygonCoordinates.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/PolygonCoordinates.cs
@@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Linq;
using System.Runtime.Serialization;
using Converters;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -18,6 +19,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
[DataContract]
[JsonConverter(typeof(PolygonCoordinatesJsonConverter))]
+ [System.Text.Json.Serialization.JsonConverter(typeof(PolygonCoordinatesSTJConverter))]
public sealed class PolygonCoordinates : IEquatable
{
///
diff --git a/Microsoft.Azure.Cosmos/src/Spatial/Position.cs b/Microsoft.Azure.Cosmos/src/Spatial/Position.cs
index de44b8895b..131e0639f7 100644
--- a/Microsoft.Azure.Cosmos/src/Spatial/Position.cs
+++ b/Microsoft.Azure.Cosmos/src/Spatial/Position.cs
@@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
using System.Linq;
using System.Runtime.Serialization;
using Converters;
+ using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;
using Newtonsoft.Json;
///
@@ -23,6 +24,7 @@ namespace Microsoft.Azure.Cosmos.Spatial
///
[DataContract]
[JsonConverter(typeof(PositionJsonConverter))]
+ [System.Text.Json.Serialization.JsonConverter(typeof(PositionSTJConverter))]
public sealed class Position : IEquatable
{
///
@@ -61,6 +63,7 @@ public Position(double longitude, double latitude, double? altitude)
{
this.Coordinates = new ReadOnlyCollection(new[] { longitude, latitude });
}
+
}
///
diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ClientTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ClientTests.cs
index bd086a3434..96d10decc9 100644
--- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ClientTests.cs
+++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ClientTests.cs
@@ -17,12 +17,14 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests
using System.Runtime.Serialization.Json;
using System.Security.Cryptography.X509Certificates;
using System.Text;
+ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using global::Azure;
using Microsoft.Azure.Cosmos.FaultInjection;
using Microsoft.Azure.Cosmos.Query.Core;
using Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests;
+ using Microsoft.Azure.Cosmos.Spatial;
using Microsoft.Azure.Cosmos.Telemetry;
using Microsoft.Azure.Cosmos.Utils;
using Microsoft.Azure.Documents;
@@ -1135,6 +1137,124 @@ public async Task CreateItemDuringTimeoutTest()
if (db != null) await db.DeleteAsync();
}
}
+
+ [TestMethod]
+ public async Task ValidateSpatialPointSTJSerialization()
+ {
+ string authKey = ConfigurationManager.AppSettings["MasterKey"];
+ string endpoint = ConfigurationManager.AppSettings["GatewayEndpoint"];
+
+ using (CosmosClient cosmosClient = new CosmosClient(endpoint, authKey,
+ new CosmosClientOptions()
+ {
+ // this makes it use STJ library for serialization/de-serialization
+ UseSystemTextJsonSerializerWithOptions = new JsonSerializerOptions()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ }
+ }
+ ))
+ {
+
+ string GUID = Guid.NewGuid().ToString();
+ Cosmos.Database database = await cosmosClient.CreateDatabaseIfNotExistsAsync("AzureCosmosSpatialSerialization");
+ Container container = await database.CreateContainerIfNotExistsAsync("spatial-items", "/id");
+
+ Point point = new Point(
+ new Position(20, 30),
+ new GeometryParams
+ {
+ AdditionalProperties = new Dictionary
+ {
+ ["one"] = "a large one",
+ ["two"] = "a new two"
+ },
+ Crs = Crs.Linked("http://foo.com", "link")
+ });
+
+ SpatialItem inputItem = new SpatialItem()
+ {
+ Id = GUID,
+ Name = "Spatial Point",
+ Location = point
+ };
+
+ SpatialItem result = await container.CreateItemAsync(inputItem);
+ SpatialItem readItem = await container.ReadItemAsync(GUID, new Cosmos.PartitionKey(GUID));
+
+ Assert.AreEqual(readItem, inputItem);
+ Assert.AreEqual(result, inputItem);
+
+ Point updatedPoint = new Point(new Position(40, 50));
+ SpatialItem patchedItem = await container.PatchItemAsync(
+ GUID,
+ new Cosmos.PartitionKey(GUID),
+ patchOperations: new[]
+ {
+ PatchOperation.Set("/location", updatedPoint)
+ });
+
+ Assert.AreEqual(updatedPoint, patchedItem.Location);
+
+ SpatialItem readAfterPatch = await container.ReadItemAsync(GUID, new Cosmos.PartitionKey(GUID));
+ Assert.AreEqual(updatedPoint, readAfterPatch.Location);
+ }
+
+ }
+ [TestMethod]
+ public async Task ValidateSpatialPointNewtonSoftSerialization()
+ {
+ string authKey = ConfigurationManager.AppSettings["MasterKey"];
+ string endpoint = ConfigurationManager.AppSettings["GatewayEndpoint"];
+ // default serialization uses NewtonSoft
+ using (CosmosClient cosmosClient = new CosmosClient(endpoint, authKey))
+ {
+
+ string GUID = Guid.NewGuid().ToString();
+ Cosmos.Database database = await cosmosClient.CreateDatabaseIfNotExistsAsync("AzureCosmosSpatialSerialization");
+ Container container = await database.CreateContainerIfNotExistsAsync("spatial-items", "/id");
+
+ Point point = new Point(
+ new Position(20, 30),
+ new GeometryParams
+ {
+ AdditionalProperties = new Dictionary
+ {
+ ["one"] = "a large one",
+ ["two"] = "a new two"
+ },
+ Crs = Crs.Linked("http://foo.com", "link")
+ });
+
+ SpatialItem inputItem = new SpatialItem()
+ {
+ Id = GUID,
+ Name = "Spatial Point",
+ Location = point
+ };
+
+ SpatialItem result = await container.CreateItemAsync(inputItem);
+ SpatialItem readItem = await container.ReadItemAsync(GUID, new Cosmos.PartitionKey(GUID));
+
+ Assert.AreEqual(readItem, inputItem);
+ Assert.AreEqual(result, inputItem);
+
+ Point updatedPoint = new Point(new Position(40, 50));
+ SpatialItem patchedItem = await container.PatchItemAsync(
+ GUID,
+ new Cosmos.PartitionKey(GUID),
+ patchOperations: new[]
+ {
+ PatchOperation.Set("/Location", updatedPoint)
+ });
+
+ Assert.AreEqual(updatedPoint, patchedItem.Location);
+
+ SpatialItem readAfterPatch = await container.ReadItemAsync(GUID, new Cosmos.PartitionKey(GUID));
+ Assert.AreEqual(updatedPoint, readAfterPatch.Location);
+ }
+
+ }
public static IReadOnlyList GetActiveConnections()
{
string testPid = Process.GetCurrentProcess().Id.ToString();
@@ -1270,6 +1390,13 @@ internal static string EscapeForSQL(this string input)
return input.Replace("'", "\\'").Replace("\"", "\\\"");
}
}
+ internal record SpatialItem
+ {
+ [JsonProperty("id")]
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public Point Location { get; set; }
+ }
internal class TestWebProxy : IWebProxy
{
diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Spatial/STJSpatialTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Spatial/STJSpatialTest.cs
new file mode 100644
index 0000000000..938414feef
--- /dev/null
+++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Spatial/STJSpatialTest.cs
@@ -0,0 +1,780 @@
+namespace Microsoft.Azure.Cosmos.Test.Spatial
+{
+ using System.Collections.Generic;
+ using Microsoft.Azure.Cosmos.Spatial;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+ using Newtonsoft.Json;
+ using Point = Cosmos.Spatial.Point;
+
+ ///
+ /// Spatial STJ serialization/deserialization tests
+ ///
+ [TestClass]
+ public class STJSpatialTest
+ {
+
+ ///
+ /// Tests serialization/deserialization of BoundingBox class.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(BoundingBoxData))]
+ public void TestBoundingBoxSerialization(BoundingBox input)
+ {
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ BoundingBox newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ BoundingBox stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(stjJson, newtonsoftJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of Position class.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(PositionData))]
+ public void TestPositionSerialization(Position input)
+ {
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ Position newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ Position stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of Crs class and its variants.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(CrsData))]
+ public void TestCrsSerialization(Crs input)
+ {
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ Crs newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ Crs stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of LinearRing class.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(LinearRingData))]
+ public void TestLinearRingSerialization(LinearRing input)
+ {
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ LinearRing newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ LinearRing stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of Point class.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(PointData))]
+ public void TestPointSerialization(Point input)
+ {
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ Point newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ Point stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+
+ ///
+ /// Tests serialization/deserialization of MultiPoint class.
+ ///
+ [TestMethod]
+ public void TestMultiPointSerialization()
+ {
+
+ MultiPoint input = new MultiPoint(
+ new[] { new Position(20, 30), new Position(30, 40) },
+ new GeometryParams
+ {
+ BoundingBox = new BoundingBox(new Position(0, 0), new Position(40, 40)),
+ Crs = Crs.Named("SomeCrs")
+ });
+
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ MultiPoint newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ MultiPoint stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of LineString class.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(LineStringData))]
+ public void TestLineStringSerialization(LineString input)
+ {
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ LineString newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ LineString stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of MultiLineString class.
+ ///
+ [TestMethod]
+ public void TestMultiLineStringSerialization()
+ {
+ MultiLineString input = new MultiLineString(
+ new[]
+ {
+ new LineStringCoordinates(new[] { new Position(20, 30), new Position(30, 40) }),
+ new LineStringCoordinates(new[] { new Position(40, 50), new Position(60, 60) })
+ },
+ new GeometryParams
+ {
+ BoundingBox = new BoundingBox(new Position(0, 0), new Position(40, 40)),
+ Crs = Crs.Named("SomeCrs")
+ });
+
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ MultiLineString newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ MultiLineString stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of Polygon class.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(PolygonData))]
+ public void TestPolygonSerialization(Polygon input)
+ {
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ Polygon newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ Polygon stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of MultiPolygon class.
+ ///
+ [TestMethod]
+ [DynamicData(nameof(MultiPolygonData))]
+ public void TestMultiPolygonSerialization(MultiPolygon input)
+ {
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ MultiPolygon newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ MultiPolygon stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of a GeometryCollection with mixed types.
+ ///
+ [TestMethod]
+ public void TestGeometricCollectionMixedTypeSerialization()
+ {
+ GeometryCollection input = new GeometryCollection(
+ new Geometry[]
+ {
+ new Point(20, 30),
+ new Polygon(new[]
+ {
+ new LinearRing(new[]
+ {
+ new Position(40, 40),
+ new Position(45, 40),
+ new Position(45, 45),
+ new Position(40, 45),
+ new Position(40, 40)
+ })
+ })
+ },
+ new GeometryParams
+ {
+ BoundingBox = new BoundingBox(new Position(0, 0), new Position(50, 50)),
+ Crs = Crs.Named("SomeCrs"),
+ AdditionalProperties = new Dictionary
+ {
+ ["one"] = "a large one",
+ ["two"] = "a new two"
+ },
+ });
+
+
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ GeometryCollection newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ GeometryCollection stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of GeometryCollection class with Point geometries.
+ ///
+ [TestMethod]
+ public void TestGeometricCollectionPointSerialization()
+ {
+ GeometryCollection input = new GeometryCollection(
+ new[] { new Point(20, 30), new Point(30, 40) },
+ new GeometryParams
+ {
+ BoundingBox = new BoundingBox(new Position(0, 0), new Position(40, 40)),
+ Crs = Crs.Named("SomeCrs"),
+ AdditionalProperties = new Dictionary
+ {
+ ["one"] = "a large one",
+ ["two"] = "a new two"
+ },
+ });
+
+
+ // Newtonsoft
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ GeometryCollection newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult);
+
+ // STJ
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ GeometryCollection stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult);
+
+ Assert.AreEqual(newtonsoftJson, stjJson);
+ }
+
+ ///
+ /// Tests serialization/deserialization of GeometryCollection containing all geometry types.
+ /// Ensures comprehensive coverage of GeometryCollection with Point, LineString, Polygon,
+ /// MultiPoint, MultiLineString, and MultiPolygon.
+ ///
+ [TestMethod]
+ public void TestGeometryCollectionWithAllGeometryTypes()
+ {
+ // Create a comprehensive GeometryCollection with all supported geometry types
+ GeometryCollection input = new GeometryCollection(
+ new Geometry[]
+ {
+ // Point
+ new Point(new Position(10, 20)),
+
+ // LineString
+ new LineString(new[]
+ {
+ new Position(20, 30),
+ new Position(30, 40)
+ }),
+
+ // Polygon
+ new Polygon(new[]
+ {
+ new LinearRing(new[]
+ {
+ new Position(0, 0),
+ new Position(0, 10),
+ new Position(10, 10),
+ new Position(10, 0),
+ new Position(0, 0)
+ })
+ }),
+
+ // MultiPoint
+ new MultiPoint(new[]
+ {
+ new Position(5, 5),
+ new Position(15, 15)
+ }),
+
+ // MultiLineString
+ new MultiLineString(new[]
+ {
+ new LineStringCoordinates(new[]
+ {
+ new Position(25, 25),
+ new Position(35, 35)
+ })
+ }),
+
+ // MultiPolygon
+ new MultiPolygon(new[]
+ {
+ new PolygonCoordinates(new[]
+ {
+ new LinearRing(new[]
+ {
+ new Position(40, 40),
+ new Position(40, 50),
+ new Position(50, 50),
+ new Position(50, 40),
+ new Position(40, 40)
+ })
+ })
+ })
+ },
+ new GeometryParams
+ {
+ BoundingBox = new BoundingBox(new Position(0, 0), new Position(50, 50)),
+ Crs = Crs.Named("EPSG:4326")
+ });
+
+ // Newtonsoft serialization/deserialization
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ GeometryCollection newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+
+ // Verify Newtonsoft round-trip by comparing JSON strings
+ string newtonsoftJson2 = JsonConvert.SerializeObject(newtonsoftResult);
+ Assert.AreEqual(newtonsoftJson, newtonsoftJson2, "Newtonsoft round-trip should produce identical JSON");
+
+ // STJ serialization/deserialization
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ GeometryCollection stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+
+ // Verify STJ round-trip by comparing JSON strings
+ string stjJson2 = System.Text.Json.JsonSerializer.Serialize(stjResult);
+ Assert.AreEqual(stjJson, stjJson2, "STJ round-trip should produce identical JSON");
+
+ // Ensure both serializers produce identical JSON output
+ Assert.AreEqual(newtonsoftJson, stjJson, "STJ and Newtonsoft should produce identical JSON");
+ }
+
+ ///
+ /// Tests serialization/deserialization of nested GeometryCollections.
+ /// Ensures that GeometryCollection can contain other GeometryCollections.
+ ///
+ [TestMethod]
+ public void TestNestedGeometryCollections()
+ {
+ // Create nested GeometryCollections
+ GeometryCollection innerCollection1 = new GeometryCollection(
+ new Geometry[]
+ {
+ new Point(10, 20),
+ new Point(30, 40)
+ });
+
+ GeometryCollection innerCollection2 = new GeometryCollection(
+ new Geometry[]
+ {
+ new LineString(new[]
+ {
+ new Position(50, 60),
+ new Position(70, 80)
+ })
+ });
+
+ GeometryCollection outerCollection = new GeometryCollection(
+ new Geometry[]
+ {
+ innerCollection1,
+ innerCollection2,
+ new Point(90, 100)
+ },
+ new GeometryParams
+ {
+ Crs = Crs.Named("SomeCrs")
+ });
+
+ // Newtonsoft serialization/deserialization
+ string newtonsoftJson = JsonConvert.SerializeObject(outerCollection);
+ GeometryCollection newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(outerCollection, newtonsoftResult, "Newtonsoft deserialized nested collection should match input");
+
+ // STJ serialization/deserialization
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(outerCollection);
+ GeometryCollection stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(outerCollection, stjResult, "STJ deserialized nested collection should match input");
+
+ // Ensure both serializers produce identical JSON output
+ Assert.AreEqual(newtonsoftJson, stjJson, "STJ and Newtonsoft should produce identical JSON for nested collections");
+ }
+
+ ///
+ /// Tests serialization/deserialization of GeometryCollection with complex coordinates.
+ /// Ensures proper handling of floating-point precision and 3D coordinates.
+ ///
+ [TestMethod]
+ public void TestGeometryCollectionWithComplexCoordinates()
+ {
+ GeometryCollection input = new GeometryCollection(
+ new Geometry[]
+ {
+ new Point(new Position(20.123456, 30.654321, 100.5)),
+ new LineString(new[]
+ {
+ new Position(-122.419, 37.775, 0),
+ new Position(-122.420, 37.776, 50.25)
+ }),
+ new Polygon(new[]
+ {
+ new LinearRing(new[]
+ {
+ new Position(0.1, 0.2),
+ new Position(0.1, 0.3),
+ new Position(0.2, 0.3),
+ new Position(0.2, 0.2),
+ new Position(0.1, 0.2)
+ })
+ })
+ },
+ new GeometryParams
+ {
+ BoundingBox = new BoundingBox(
+ new Position(-122.420, 37.775, 0),
+ new Position(20.123456, 37.776, 100.5)),
+ AdditionalProperties = new Dictionary
+ {
+ ["precision"] = "high",
+ ["elevation"] = true
+ }
+ });
+
+ // Newtonsoft serialization/deserialization
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ GeometryCollection newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult, "Newtonsoft should handle complex coordinates");
+
+ // STJ serialization/deserialization
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ GeometryCollection stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult, "STJ should handle complex coordinates");
+
+ // Ensure both serializers produce identical JSON output
+ Assert.AreEqual(newtonsoftJson, stjJson, "Both serializers should produce identical JSON for complex coordinates");
+ }
+
+ ///
+ /// Tests serialization/deserialization of GeometryCollection without optional properties.
+ /// Ensures proper handling when BoundingBox, Crs, and AdditionalProperties are null.
+ ///
+ [TestMethod]
+ public void TestGeometryCollectionWithoutOptionalProperties()
+ {
+ // Create a minimal GeometryCollection without optional properties
+ GeometryCollection input = new GeometryCollection(
+ new Geometry[]
+ {
+ new Point(10, 20),
+ new LineString(new[] { new Position(30, 40), new Position(50, 60) })
+ });
+
+ // Newtonsoft serialization/deserialization
+ string newtonsoftJson = JsonConvert.SerializeObject(input);
+ GeometryCollection newtonsoftResult = JsonConvert.DeserializeObject(newtonsoftJson);
+ Assert.AreEqual(input, newtonsoftResult, "Newtonsoft should handle GeometryCollection without optional properties");
+
+ // STJ serialization/deserialization
+ string stjJson = System.Text.Json.JsonSerializer.Serialize(input);
+ GeometryCollection stjResult = System.Text.Json.JsonSerializer.Deserialize(stjJson);
+ Assert.AreEqual(input, stjResult, "STJ should handle GeometryCollection without optional properties");
+
+ // Ensure both serializers produce identical JSON output
+ Assert.AreEqual(newtonsoftJson, stjJson, "Both serializers should produce identical JSON without optional properties");
+ }
+
+ public static IEnumerable