Microsoft.Azure.Cosmos/src/Json/JsonSerializer.cs (480 lines of code) (raw):
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------
namespace Microsoft.Azure.Cosmos.Json
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Azure.Cosmos.CosmosElements;
using Microsoft.Azure.Cosmos.Query.Core.Monads;
internal static class JsonSerializer
{
public static ReadOnlyMemory<byte> Serialize(
object value,
JsonSerializationFormat jsonSerializationFormat = JsonSerializationFormat.Text)
{
IJsonWriter jsonWriter = JsonWriter.Create(jsonSerializationFormat);
JsonSerializer.SerializeInternal(value, jsonWriter);
return jsonWriter.GetResult();
}
public static void SerializeInternal(
object value,
IJsonWriter jsonWriter)
{
if (jsonWriter == null)
{
throw new ArgumentNullException(nameof(jsonWriter));
}
switch (value)
{
case null:
jsonWriter.WriteNullValue();
break;
case bool boolValue:
jsonWriter.WriteBoolValue(boolValue);
break;
case string stringValue:
jsonWriter.WriteStringValue(stringValue);
break;
case Number64 numberValue:
jsonWriter.WriteNumberValue(numberValue);
break;
case sbyte signedByteValue:
jsonWriter.WriteInt8Value(signedByteValue);
break;
case short shortValue:
jsonWriter.WriteInt16Value(shortValue);
break;
case int intValue:
jsonWriter.WriteInt32Value(intValue);
break;
case long longValue:
jsonWriter.WriteInt64Value(longValue);
break;
case uint uintValue:
jsonWriter.WriteUInt32Value(uintValue);
break;
case float floatValue:
jsonWriter.WriteFloat32Value(floatValue);
break;
case double doubleValue:
jsonWriter.WriteFloat64Value(doubleValue);
break;
case ReadOnlyMemory<byte> binaryValue:
jsonWriter.WriteBinaryValue(binaryValue.Span);
break;
case Guid guidValue:
jsonWriter.WriteGuidValue(guidValue);
break;
case IEnumerable enumerableValue:
jsonWriter.WriteArrayStart();
foreach (object arrayItem in enumerableValue)
{
JsonSerializer.SerializeInternal(arrayItem, jsonWriter);
}
jsonWriter.WriteArrayEnd();
break;
case CosmosElement cosmosElementValue:
cosmosElementValue.WriteTo(jsonWriter);
break;
case ValueType valueType:
throw new ArgumentOutOfRangeException($"Unable to serialize type: {valueType.GetType()}");
default:
Type type = value.GetType();
PropertyInfo[] properties = type.GetProperties();
jsonWriter.WriteObjectStart();
foreach (PropertyInfo propertyInfo in properties)
{
jsonWriter.WriteFieldName(propertyInfo.Name);
object propertyValue = propertyInfo.GetValue(value);
JsonSerializer.SerializeInternal(propertyValue, jsonWriter);
}
jsonWriter.WriteObjectEnd();
break;
}
}
public static T Deserialize<T>(ReadOnlyMemory<byte> buffer)
{
TryCatch<T> tryDeserialize = JsonSerializer.Monadic.Deserialize<T>(buffer);
tryDeserialize.ThrowIfFailed();
return tryDeserialize.Result;
}
public static class Monadic
{
public static TryCatch<T> Deserialize<T>(ReadOnlyMemory<byte> buffer)
{
TryCatch<CosmosElement> tryCreateFromBuffer = CosmosElement.Monadic.CreateFromBuffer(buffer);
if (tryCreateFromBuffer.Failed)
{
return TryCatch<T>.FromException(tryCreateFromBuffer.Exception);
}
CosmosElement cosmosElement = tryCreateFromBuffer.Result;
TryCatch<object> tryAcceptVisitor = cosmosElement.Accept(DeserializationVisitor.Singleton, typeof(T));
if (tryAcceptVisitor.Failed)
{
return TryCatch<T>.FromException(tryAcceptVisitor.Exception);
}
if (!(tryAcceptVisitor.Result is T typedResult))
{
Type type = typeof(T);
if ((tryAcceptVisitor.Result is null) && (!type.IsValueType || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))))
{
return TryCatch<T>.FromResult(default);
}
// string needs to be handle differently since JsonReader return UtfAnyString instead of string.
if (type == typeof(string))
{
return TryCatch<T>.FromResult((T)(object)tryAcceptVisitor.Result.ToString());
}
throw new InvalidOperationException("Could not cast to T.");
}
return TryCatch<T>.FromResult(typedResult);
}
}
public static bool TryDeserialize<T>(ReadOnlyMemory<byte> buffer, out T result)
{
TryCatch<T> tryDeserialize = JsonSerializer.Monadic.Deserialize<T>(buffer);
return TryCatch<T>.ConvertToTryGet<T>(tryDeserialize, out result);
}
private sealed class DeserializationVisitor : ICosmosElementVisitor<Type, TryCatch<object>>
{
public static readonly DeserializationVisitor Singleton = new DeserializationVisitor();
private static class Exceptions
{
public static readonly CosmosElementWrongTypeException ExpectedArray = new CosmosElementWrongTypeException(
message: $"Expected return type of '{nameof(IReadOnlyList<object>)}'.");
public static readonly CosmosElementWrongTypeException ExpectedBoolean = new CosmosElementWrongTypeException(
message: $"Expected return type of '{typeof(bool)}'.");
public static readonly CosmosElementWrongTypeException ExpectedBinary = new CosmosElementWrongTypeException(
message: $"Expected return type of '{typeof(ReadOnlyMemory<byte>)}'.");
public static readonly CosmosElementWrongTypeException ExpectedGuid = new CosmosElementWrongTypeException(
message: $"Expected return type of '{typeof(Guid)}'.");
public static readonly CosmosElementWrongTypeException ExpectedNumber = new CosmosElementWrongTypeException(
message: "Expected return type of number.");
public static readonly CosmosElementWrongTypeException ExpectedReferenceOrNullableType = new CosmosElementWrongTypeException(
message: "Expected return type to be a reference or nullable type.");
public static readonly CosmosElementWrongTypeException ExpectedString = new CosmosElementWrongTypeException(
message: $"Expected return type of '{typeof(string)}'.");
public static readonly CosmosElementWrongTypeException UnexpectedUndefined = new CosmosElementWrongTypeException(
message: $"Did not expect to encounter '{typeof(CosmosUndefined)}'.");
}
private static class BoxedValues
{
public static readonly object True = true;
public static readonly object False = false;
public static readonly object Null = null;
}
private DeserializationVisitor()
{
}
public TryCatch<object> Visit(CosmosArray cosmosArray, Type type)
{
bool isReadOnlyList = type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>));
if (!isReadOnlyList)
{
return TryCatch<object>.FromException(DeserializationVisitor.Exceptions.ExpectedArray);
}
Type genericArgumentType = type.GenericTypeArguments.First();
Type listType = typeof(List<>).MakeGenericType(genericArgumentType);
IList list = (IList)Activator.CreateInstance(listType);
foreach (CosmosElement arrayItem in cosmosArray)
{
TryCatch<object> tryGetMaterializedArrayItem;
if (genericArgumentType == typeof(object))
{
Type dotNetType = arrayItem switch
{
CosmosArray _ => typeof(IReadOnlyList<object>),
CosmosBoolean _ => typeof(bool),
CosmosNull _ => typeof(object),
CosmosNumber _ => typeof(Number64),
CosmosObject _ => typeof(object),
CosmosString _ => typeof(string),
CosmosGuid _ => typeof(Guid),
CosmosBinary _ => typeof(ReadOnlyMemory<byte>),
CosmosUndefined _ => typeof(object),
_ => throw new ArgumentOutOfRangeException($"Unknown cosmos element type."),
};
tryGetMaterializedArrayItem = arrayItem.Accept(this, dotNetType);
}
else
{
tryGetMaterializedArrayItem = arrayItem.Accept(this, genericArgumentType);
}
if (tryGetMaterializedArrayItem.Failed)
{
return tryGetMaterializedArrayItem;
}
list.Add(tryGetMaterializedArrayItem.Result);
}
return TryCatch<object>.FromResult(list);
}
public TryCatch<object> Visit(CosmosBinary cosmosBinary, Type type)
{
if (type != typeof(ReadOnlyMemory<byte>))
{
return TryCatch<object>.FromException(DeserializationVisitor.Exceptions.ExpectedBinary);
}
return TryCatch<object>.FromResult(cosmosBinary.Value);
}
public TryCatch<object> Visit(CosmosBoolean cosmosBoolean, Type type)
{
if (type != typeof(bool))
{
return TryCatch<object>.FromException(DeserializationVisitor.Exceptions.ExpectedBoolean);
}
return TryCatch<object>.FromResult(cosmosBoolean.Value ? DeserializationVisitor.BoxedValues.True : DeserializationVisitor.BoxedValues.False);
}
public TryCatch<object> Visit(CosmosGuid cosmosGuid, Type type)
{
if (type != typeof(Guid))
{
return TryCatch<object>.FromException(DeserializationVisitor.Exceptions.ExpectedGuid);
}
return TryCatch<object>.FromResult(cosmosGuid.Value);
}
public TryCatch<object> Visit(CosmosNull cosmosNull, Type type)
{
if (type.IsValueType && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)))
{
return TryCatch<object>.FromException(DeserializationVisitor.Exceptions.ExpectedReferenceOrNullableType);
}
return TryCatch<object>.FromResult(default);
}
public TryCatch<object> Visit(CosmosUndefined cosmosUndefined, Type type)
{
return TryCatch<object>.FromException(DeserializationVisitor.Exceptions.UnexpectedUndefined);
}
public TryCatch<object> Visit(CosmosNumber cosmosNumber, Type type)
{
if (type == typeof(Number64))
{
return TryCatch<object>.FromResult(cosmosNumber.Value);
}
switch (Type.GetTypeCode(type))
{
case TypeCode.Byte:
{
if (!cosmosNumber.Value.IsInteger)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected integral type for byte."));
}
long value = Number64.ToLong(cosmosNumber.Value);
if ((value < byte.MinValue) || (value > byte.MaxValue))
{
return TryCatch<object>.FromException(
new OverflowException($"{value} was out of range for byte."));
}
return TryCatch<object>.FromResult((byte)value);
}
case TypeCode.Decimal:
{
decimal value;
if (cosmosNumber.Value.IsDouble)
{
value = (decimal)Number64.ToDouble(cosmosNumber.Value);
}
else
{
value = Number64.ToLong(cosmosNumber.Value);
}
return TryCatch<object>.FromResult(value);
}
case TypeCode.Double:
{
if (!cosmosNumber.Value.IsDouble)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected floating point type for double."));
}
double value = Number64.ToDouble(cosmosNumber.Value);
return TryCatch<object>.FromResult(value);
}
case TypeCode.Int16:
{
if (!cosmosNumber.Value.IsInteger)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected integral type for short."));
}
long value = Number64.ToLong(cosmosNumber.Value);
if ((value < short.MinValue) || (value > short.MaxValue))
{
return TryCatch<object>.FromException(
new OverflowException($"{value} was out of range for short."));
}
return TryCatch<object>.FromResult((short)value);
}
case TypeCode.Int32:
{
if (!cosmosNumber.Value.IsInteger)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected integral type for int."));
}
long value = Number64.ToLong(cosmosNumber.Value);
if ((value < int.MinValue) || (value > int.MaxValue))
{
return TryCatch<object>.FromException(
new OverflowException($"{value} was out of range for int."));
}
return TryCatch<object>.FromResult((int)value);
}
case TypeCode.Int64:
{
if (!cosmosNumber.Value.IsInteger)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected integral type for long."));
}
long value = Number64.ToLong(cosmosNumber.Value);
return TryCatch<object>.FromResult(value);
}
case TypeCode.SByte:
{
if (!cosmosNumber.Value.IsInteger)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected integral type for sbyte."));
}
long value = Number64.ToLong(cosmosNumber.Value);
if ((value < sbyte.MinValue) || (value > sbyte.MaxValue))
{
return TryCatch<object>.FromException(
new OverflowException($"{value} was out of range for sbyte."));
}
return TryCatch<object>.FromResult((sbyte)value);
}
case TypeCode.Single:
{
if (!cosmosNumber.Value.IsDouble)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected floating point type for float."));
}
double value = Number64.ToDouble(cosmosNumber.Value);
if ((value < float.MinValue) || (value > float.MaxValue))
{
return TryCatch<object>.FromException(
new OverflowException($"{value} was out of range for float."));
}
return TryCatch<object>.FromResult((float)value);
}
case TypeCode.UInt16:
{
if (!cosmosNumber.Value.IsInteger)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected integral type for ushort."));
}
long value = Number64.ToLong(cosmosNumber.Value);
if ((value < ushort.MinValue) || (value > ushort.MaxValue))
{
return TryCatch<object>.FromException(
new OverflowException($"{value} was out of range for ushort."));
}
return TryCatch<object>.FromResult((ushort)value);
}
case TypeCode.UInt32:
{
if (!cosmosNumber.Value.IsInteger)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected integral type for uint."));
}
long value = Number64.ToLong(cosmosNumber.Value);
if ((value < uint.MinValue) || (value > uint.MaxValue))
{
return TryCatch<object>.FromException(
new OverflowException($"{value} was out of range for uint."));
}
return TryCatch<object>.FromResult((uint)value);
}
case TypeCode.UInt64:
{
if (!cosmosNumber.Value.IsInteger)
{
return TryCatch<object>.FromException(
new CosmosElementWrongTypeException("Expected integral type for ulong."));
}
long value = Number64.ToLong(cosmosNumber.Value);
return TryCatch<object>.FromResult((ulong)value);
}
default:
throw new ArgumentOutOfRangeException($"Unknown {nameof(TypeCode)}: {Type.GetTypeCode(type)}.");
}
}
public TryCatch<object> Visit(CosmosObject cosmosObject, Type type)
{
ConstructorInfo[] constructors = type.GetConstructors();
if (constructors.Length == 0)
{
return TryCatch<object>.FromException(
new CosmosElementNoPubliclyAccessibleConstructorException(
message: $"Could not find publicly accessible constructors for type: {type.FullName}."));
}
if (constructors.Length > 1)
{
return TryCatch<object>.FromException(
new CosmosElementCouldNotDetermineWhichConstructorToUseException(
message: $"Could not determine which constructor to use for type: {type.FullName}."));
}
ConstructorInfo constructor = constructors.First();
ParameterInfo[] parameters = constructor.GetParameters();
List<object> parameterValues = new List<object>();
foreach (ParameterInfo parameter in parameters)
{
if (!cosmosObject.TryGetValue(parameter.Name, out CosmosElement rawParameterValue))
{
return TryCatch<object>.FromException(
new CosmosElementFailedToFindPropertyException(
message: $"Could not find property: '{parameter.Name}'."));
}
TryCatch<object> tryGetMaterializedParameterValue = rawParameterValue.Accept(this, parameter.ParameterType);
if (tryGetMaterializedParameterValue.Failed)
{
return TryCatch<object>.FromException(tryGetMaterializedParameterValue.Exception);
}
parameterValues.Add(tryGetMaterializedParameterValue.Result);
}
object instance;
try
{
instance = constructor.Invoke(parameterValues.ToArray());
}
catch (Exception ex)
{
return TryCatch<object>.FromException(ex);
}
return TryCatch<object>.FromResult(instance);
}
public TryCatch<object> Visit(CosmosString cosmosString, Type type)
{
if (type != typeof(string))
{
return TryCatch<object>.FromException(DeserializationVisitor.Exceptions.ExpectedString);
}
return TryCatch<object>.FromResult(cosmosString.Value.ToString());
}
}
private sealed class ReadOnlyListWrapper<T> : IReadOnlyList<T>
{
private readonly IList<T> list;
public ReadOnlyListWrapper(IList<T> list)
{
this.list = list ?? throw new ArgumentNullException(nameof(list));
}
public T this[int index] => this.list[index];
public int Count => this.list.Count;
public IEnumerator<T> GetEnumerator()
{
return this.list.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.list.GetEnumerator();
}
}
}
}