Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SpatialPoint Serialization/Deserialization: Fixes spatial point serialization/deserialization bug #4801

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Azure.Cosmos.Spatial.Converters.STJConverters;

/// <summary>
/// This class provides a default implementation of System.Text.Json Cosmos Linq Serializer.
Expand All @@ -29,6 +30,8 @@ internal CosmosSystemTextJsonSerializer(
JsonSerializerOptions jsonSerializerOptions)
{
this.jsonSerializerOptions = jsonSerializerOptions;
//somehow adding a .NET native type converter(in this case Dictionary) as an attribute on properties doesnt take effect. So adding it here instead
this.jsonSerializerOptions.Converters.Add(new DictionarySTJConverter());
}

/// <inheritdoc/>
Expand Down Expand Up @@ -59,7 +62,7 @@ public override Stream ToStream<T>(T input)
{
MemoryStream streamPayload = new ();
using Utf8JsonWriter writer = new (streamPayload);

JsonSerializer.Serialize(writer, input, this.jsonSerializerOptions);

streamPayload.Position = 0;
Expand Down
2 changes: 2 additions & 0 deletions Microsoft.Azure.Cosmos/src/Spatial/BoundingBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ 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;

/// <summary>
/// Represents a coordinate range for geometries in the Azure Cosmos DB service.
/// </summary>
[DataContract]
[JsonConverter(typeof(BoundingBoxJsonConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(BoundingBoxSTJConverter))]
public sealed class BoundingBox : IEquatable<BoundingBox>
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// ------------------------------------------------------------
// 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;
/// <summary>
/// Converter used to support System.Text.Json de/serialization of type BoundingBox/>.
/// </summary>
internal class BoundingBoxSTJConverter : JsonConverter<BoundingBox>
{
public override BoundingBox Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException(RMResources.JsonUnexpectedToken);
}
Position min = null;
Position max = null;

JsonElement rootElement = JsonDocument.ParseValue(ref reader).RootElement;
foreach (JsonProperty property in rootElement.EnumerateObject())
{
if (property.NameEquals(STJMetaDataFields.Min))
{
min = JsonSerializer.Deserialize<Position>(property.Value.GetRawText(), options);
}
else if (property.NameEquals(STJMetaDataFields.Max))
{
max = JsonSerializer.Deserialize<Position>(property.Value.GetRawText(), options);

}
}

return new BoundingBox(min, max);
}

public override void Write(Utf8JsonWriter writer, BoundingBox box, JsonSerializerOptions options)
{
writer.WriteStartObject(STJMetaDataFields.BoundingBox);

writer.WriteStartObject(STJMetaDataFields.Min);
JsonSerializer.Serialize(writer, box.Min, options);
writer.WriteEndObject();

writer.WriteStartObject(STJMetaDataFields.Max);
JsonSerializer.Serialize(writer, box.Max, options);
writer.WriteEndObject();

writer.WriteEndObject();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// ------------------------------------------------------------
// 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;
/// <summary>
/// Converter used to support System.Text.Json de/serialization of type Crs/>.
/// </summary>
internal class CrsSTJConverter : JsonConverter<Crs>
{
public override Crs Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException(RMResources.JsonUnexpectedToken);
}

JsonElement rootElement = JsonDocument.ParseValue(ref reader).RootElement;
JsonElement properties = rootElement.GetProperty(STJMetaDataFields.Properties);
if (properties.ValueKind == JsonValueKind.Null || properties.ValueKind != JsonValueKind.Object)
{
throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
}

JsonElement crsType = rootElement.GetProperty(STJMetaDataFields.Type);
if (crsType.ValueKind == JsonValueKind.Null || crsType.ValueKind != JsonValueKind.String)
{
throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
}

switch (crsType.GetString())
{
case STJMetaDataFields.Name:
string crsName = properties.GetProperty(STJMetaDataFields.Name).GetString();
return new NamedCrs(crsName);

case STJMetaDataFields.Link:
string crsHref = properties.GetProperty(STJMetaDataFields.Href).GetString();
if (properties.TryGetProperty(STJMetaDataFields.Type, out JsonElement crsHrefType))
{
return new LinkedCrs(crsHref, crsHrefType.GetString());
}
return new LinkedCrs(crsHref);

default:
throw new JsonException(RMResources.SpatialFailedToDeserializeCrs);
}

}
public override void Write(Utf8JsonWriter writer, Crs crs, JsonSerializerOptions options)
{
switch (crs.Type)
{
case CrsType.Linked:
writer.WriteStartObject(STJMetaDataFields.Crs);
LinkedCrs linkedCrs = (LinkedCrs)crs;
writer.WriteString(STJMetaDataFields.Type, STJMetaDataFields.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(STJMetaDataFields.Crs);
NamedCrs namedCrs = (NamedCrs)crs;
writer.WriteString(STJMetaDataFields.Type, STJMetaDataFields.Name);
writer.WritePropertyName(STJMetaDataFields.Properties);
writer.WriteStartObject();
writer.WriteString(STJMetaDataFields.Name, namedCrs.Name);
writer.WriteEndObject();

writer.WriteEndObject();
break;

case CrsType.Unspecified:
writer.WriteNull(STJMetaDataFields.Crs);
break;
}

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// ------------------------------------------------------------
// 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.Documents;
// System.Text.Json by default returns JsonElement as a value type for any key in the dictionary as it cant infer the type of value . This Converter is required to translate JsonElement to .NET type to return back to the client.
internal class DictionarySTJConverter : JsonConverter<IDictionary<string, object>>
{
public override IDictionary<string, object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException(RMResources.JsonUnexpectedToken);
}
Dictionary<string, object> dictionary = new Dictionary<string, object>();

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return dictionary;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException(RMResources.JsonUnexpectedToken);
}
string propertyName = reader.GetString();
reader.Read(); // get the value
object value = this.getValue(ref reader, options);
dictionary.Add(propertyName, value);
}

return dictionary;

}
public override void Write(Utf8JsonWriter writer, IDictionary<string, object> dict, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, (Dictionary<string, object>)dict, options);

}

private object getValue(ref Utf8JsonReader reader, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
JsonTokenType.String => reader.GetString(),
JsonTokenType.True => true,
JsonTokenType.False => false,
JsonTokenType.Number when reader.TryGetInt32(out int i) => i,
JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
JsonTokenType.Number => reader.GetDouble(),
JsonTokenType.Null => null,
JsonTokenType.StartObject => this.Read(ref reader, null!, options),
_ => throw new JsonException(RMResources.JsonUnexpectedToken)
};
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// ------------------------------------------------------------
// 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;
using Point = Point;
/// <summary>
/// Converter used to support System.Text.Json de/serialization of type GeometryCollection/>.
/// </summary>
internal class GeometryCollectionSTJConverter : JsonConverter<GeometryCollection>
{
public override GeometryCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException(RMResources.JsonUnexpectedToken);
}
IList<Geometry> geometries = null;
JsonElement rootElement = JsonDocument.ParseValue(ref reader).RootElement;
if (rootElement.TryGetProperty(STJMetaDataFields.Geometries, out JsonElement value))
{
geometries = new List<Geometry>();
foreach (JsonElement arrayElement in value.EnumerateArray())
{
GeometryShape shape = (GeometryShape)arrayElement.GetProperty("type").GetInt16();
Type type = shape.GetType();

switch (shape.ToString())
{
case nameof(GeometryShape.Point):
Point point = JsonSerializer.Deserialize<Point>(arrayElement.GetRawText(), options);
geometries.Add(point);
break;

case nameof(GeometryShape.MultiPoint):
MultiPoint multiPoint = JsonSerializer.Deserialize<MultiPoint>(arrayElement.GetRawText(), options);
geometries.Add(multiPoint);
break;

case nameof(GeometryShape.LineString):
LineString lineString = JsonSerializer.Deserialize<LineString>(arrayElement.GetRawText(), options);
geometries.Add(lineString);
break;

case nameof(GeometryShape.MultiLineString):
MultiLineString multiLineString = JsonSerializer.Deserialize<MultiLineString>(arrayElement.GetRawText(), options);
geometries.Add(multiLineString);
break;

case nameof(GeometryShape.Polygon):
Polygon polygon = JsonSerializer.Deserialize<Polygon>(arrayElement.GetRawText(), options);
geometries.Add(polygon);
break;

case nameof(GeometryShape.MultiPolygon):
MultiPolygon multiPolygon = JsonSerializer.Deserialize<MultiPolygon>(arrayElement.GetRawText(), options);
geometries.Add(multiPolygon);
break;

case nameof(GeometryShape.GeometryCollection):
GeometryCollection geometryCollection = JsonSerializer.Deserialize<GeometryCollection>(arrayElement.GetRawText(), options);
geometries.Add(geometryCollection);
break;

default:
throw new JsonException(RMResources.SpatialInvalidGeometryType);

}
}
}

(IDictionary<string, object> additionalProperties, Crs crs, BoundingBox boundingBox) = SpatialHelper.DeSerializePartialSpatialObject(rootElement, options);
return new GeometryCollection(geometries, new GeometryParams
{
AdditionalProperties = additionalProperties,
BoundingBox = boundingBox,
Crs = crs
});

}
public override void Write(Utf8JsonWriter writer, GeometryCollection geometryCollection, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteStartArray(STJMetaDataFields.Geometries);
foreach (Geometry geometry in geometryCollection.Geometries)
{
if (geometry.GetType() == typeof(Point))
{
JsonSerializer.Serialize(writer, (Point)geometry, options);
}
else if (geometry.GetType() == typeof(MultiPoint))
{
JsonSerializer.Serialize(writer, (MultiPoint)geometry, options);
}
else if (geometry.GetType() == typeof(LineString))
{
JsonSerializer.Serialize(writer, (LineString)geometry, options);
}
else if (geometry.GetType() == typeof(MultiLineString))
{
JsonSerializer.Serialize(writer, (MultiLineString)geometry, options);
}
else if (geometry.GetType() == typeof(Polygon))
{
JsonSerializer.Serialize(writer, (Polygon)geometry, options);
}
else if (geometry.GetType() == typeof(MultiPolygon))
{
JsonSerializer.Serialize(writer, (MultiPolygon)geometry, options);
}
else if (geometry.GetType() == typeof(GeometryCollection))
{
JsonSerializer.Serialize(writer, (GeometryCollection)geometry, options);
}

}

writer.WriteEndArray();

SpatialHelper.SerializePartialSpatialObject(geometryCollection.Crs, (int)geometryCollection.Type, geometryCollection.BoundingBox, geometryCollection.AdditionalProperties, writer, options);
writer.WriteEndObject();

}

}
}
Loading