Skip to content

Add serialization support for System.Text.Json (Attempt #2) #966

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,13 @@ private class TestConverter : UnitsNetBaseJsonConverter<string>

public (string Unit, double Value) Test_ConvertDoubleIQuantity(IQuantity value)
{
var result = ConvertIQuantity(value);
var result = BaseConverter.ConvertIQuantity(value, CreateValueUnit, CreateExtendedValueUnit);
return (result.Unit, result.Value);
}

public (string Unit, decimal Value) Test_ConvertDecimalIQuantity(IQuantity value)
{
var result = ConvertIQuantity(value);
var result = BaseConverter.ConvertIQuantity(value, CreateValueUnit, CreateExtendedValueUnit);
if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
{
return (result.Unit, decimal.Parse(decimalResult.ValueString, CultureInfo.InvariantCulture));
Expand All @@ -253,7 +253,8 @@ public IQuantity Test_ConvertDecimalValueUnit(string unit, decimal value) => Tes
});

public IQuantity Test_ConvertValueUnit() => Test_ConvertValueUnit(null);
private IQuantity Test_ConvertValueUnit(ValueUnit valueUnit) => ConvertValueUnit(valueUnit);

private IQuantity Test_ConvertValueUnit(ValueUnit valueUnit) => BaseConverter.ConvertValueUnit(valueUnit);

public JsonSerializer Test_CreateLocalSerializer(JsonSerializer serializer) => CreateLocalSerializer(serializer, this);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@

<!-- Project references, will also generate the corresponding nuget dependencies -->
<ItemGroup>
<ProjectReference Include="..\UnitsNet.Serialization\UnitsNet.Serialization.csproj" />
<ProjectReference Include="..\UnitsNet\UnitsNet.csproj" />
</ItemGroup>

<!-- Files to include in nuget package -->
<ItemGroup>
<None Include="../Docs/Images/logo-32.png" Pack="true" PackagePath="/"/>
<None Include="../Docs/Images/logo-32.png" Pack="true" PackagePath="/" />
</ItemGroup>

</Project>
171 changes: 23 additions & 148 deletions UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.

using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
Expand All @@ -18,28 +16,27 @@ namespace UnitsNet.Serialization.JsonNet
/// <typeparam name="T">The type being converted. Should either be <see cref="IQuantity"/> or <see cref="IComparable"/></typeparam>
public abstract class UnitsNetBaseJsonConverter<T> : JsonConverter<T>
{
private ConcurrentDictionary<string, (Type Quantity, Type Unit)> _registeredTypes = new();

/// <summary>
/// Register custom types so that the converter can instantiate these quantities.
/// Instead of calling <see cref="Quantity.From"/>, the <see cref="Activator"/> will be used to instantiate the object.
/// It is therefore assumed that the constructor of <paramref name="quantity"/> is specified with <c>new T(double value, typeof(<paramref name="unit"/>) unit)</c>.
/// Registering the same <paramref name="unit"/> multiple times, it will overwrite the one registered.
/// Base converter functionality
/// </summary>
protected readonly QuantityConverter BaseConverter = new();

/// <inheritdoc cref="QuantityConverter.RegisterCustomType"/>
public void RegisterCustomType(Type quantity, Type unit)
{
if (!typeof(T).IsAssignableFrom(quantity))
{
throw new ArgumentException($"The type {quantity} is not a {typeof(T)}");
}
BaseConverter.RegisterCustomType(quantity, unit);
}

if (!typeof(Enum).IsAssignableFrom(unit))
{
throw new ArgumentException($"The type {unit} is not a {nameof(Enum)}");
}
/// <summary>
/// Factory method to create a <see cref="ValueUnit"/>
/// </summary>
protected static ValueUnit CreateValueUnit(string unit, double value) => new ValueUnit { Unit = unit, Value = value };

_registeredTypes[unit.Name] = (quantity, unit);
}
/// <summary>
/// Factory method to create a <see cref="ExtendedValueUnit"/>
/// </summary>
protected static ExtendedValueUnit CreateExtendedValueUnit(string unit, double value, string valueString, string valueType)
=> new ExtendedValueUnit { Unit = unit, Value = value, ValueString = valueString, ValueType = valueType};

/// <summary>
/// Reads the "Unit" and "Value" properties from a JSON string
Expand Down Expand Up @@ -89,110 +86,6 @@ protected ValueUnit ReadValueUnit(JToken jsonToken)
};
}

/// <summary>
/// Convert a <see cref="ValueUnit"/> to an <see cref="IQuantity"/>
/// </summary>
/// <param name="valueUnit">The value unit to convert</param>
/// <exception cref="UnitsNetException">Thrown when an invalid Unit has been provided</exception>
/// <returns>An IQuantity</returns>
protected IQuantity ConvertValueUnit(ValueUnit valueUnit)
{
if (string.IsNullOrWhiteSpace(valueUnit?.Unit))
{
return null;
}

var unit = GetUnit(valueUnit.Unit);
var registeredQuantity = GetRegisteredType(valueUnit.Unit).Quantity;

if (registeredQuantity is not null)
{
return (IQuantity)Activator.CreateInstance(registeredQuantity, valueUnit.Value, unit);
}

return valueUnit switch
{
ExtendedValueUnit {ValueType: "decimal"} extendedValueUnit => Quantity.From(decimal.Parse(extendedValueUnit.ValueString, CultureInfo.InvariantCulture), unit),
_ => Quantity.From(valueUnit.Value, unit)
};
}

private (Type Quantity, Type Unit) GetRegisteredType(string unit)
{
(var unitEnumTypeName, var _) = SplitUnitString(unit);
if (_registeredTypes.TryGetValue(unitEnumTypeName, out var registeredType))
{
return registeredType;
}

return (null, null);
}

private Enum GetUnit(string unit)
{
(var unitEnumTypeName, var unitEnumValue) = SplitUnitString(unit);

// First try to find the name in the list of registered types.
var unitEnumType = GetRegisteredType(unit).Unit;

if (unitEnumType is null)
{
// "UnitsNet.Units.MassUnit,UnitsNet"
var unitEnumTypeAssemblyQualifiedName = "UnitsNet.Units." + unitEnumTypeName + ",UnitsNet";

// -- see http://stackoverflow.com/a/6465096/1256096 for details
unitEnumType = Type.GetType(unitEnumTypeAssemblyQualifiedName);

if (unitEnumType is null)
{
var ex = new UnitsNetException("Unable to find enum type.");
ex.Data["type"] = unitEnumTypeAssemblyQualifiedName;
throw ex;
}
}

var unitValue = (Enum) Enum.Parse(unitEnumType, unitEnumValue); // Ex: MassUnit.Kilogram
return unitValue;
}

private static (string EnumName, string EnumValue) SplitUnitString(string unit)
{
var unitParts = unit.Split('.');

if (unitParts.Length != 2)
{
var ex = new UnitsNetException($"\"{unit}\" is not a valid unit.");
ex.Data["type"] = unit;
throw ex;
}

// "MassUnit.Kilogram" => "MassUnit" and "Kilogram"
return (unitParts[0], unitParts[1]);
}

/// <summary>
/// Convert an <see cref="IQuantity"/> to a <see cref="ValueUnit"/>
/// </summary>
/// <param name="quantity">The quantity to convert</param>
/// <returns>A serializable object.</returns>
protected ValueUnit ConvertIQuantity(IQuantity quantity)
{
quantity = quantity ?? throw new ArgumentNullException(nameof(quantity));

if (quantity is IDecimalQuantity d)
{
return new ExtendedValueUnit
{
Unit = $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}",
Value = quantity.Value,
ValueString = d.Value.ToString(CultureInfo.InvariantCulture),
ValueType = "decimal"
};
}

return new ValueUnit {Value = quantity.Value, Unit = $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}"};
}

/// <summary>
/// Create a copy of a serializer, retaining any settings but leaving out a converter to prevent loops
/// </summary>
Expand Down Expand Up @@ -240,44 +133,26 @@ protected JsonSerializer CreateLocalSerializer(JsonSerializer serializer, JsonCo
return localSerializer;
}

/// <summary>
/// A structure used to serialize/deserialize Units.NET unit instances.
/// </summary>
protected class ValueUnit
/// <inheritdoc cref="IValueUnit"/>
protected class ValueUnit: IValueUnit
{
/// <summary>
/// The unit of the value.
/// </summary>
/// <example>MassUnit.Pound</example>
/// <example>InformationUnit.Kilobyte</example>
/// <inheritdoc cref="IValueUnit.Unit"/>
[JsonProperty(Order = 1)]
public string Unit { get; [UsedImplicitly] set; }

/// <summary>
/// The value.
/// </summary>
/// <inheritdoc cref="IValueUnit.Value"/>
[JsonProperty(Order = 2)]
public double Value { get; [UsedImplicitly] set; }
}

/// <summary>
/// A structure used to serialize/deserialize non-double Units.NET unit instances.
/// </summary>
/// <remarks>
/// This type was added for lossless serialization of quantities with <see cref="decimal"/> values.
/// The <see cref="decimal"/> type distinguishes between 100 and 100.00 but Json.NET does not, therefore we serialize decimal values as string.
/// </remarks>
protected sealed class ExtendedValueUnit : ValueUnit
/// <inheritdoc cref="IExtendedValueUnit"/>
protected sealed class ExtendedValueUnit : ValueUnit, IExtendedValueUnit
{
/// <summary>
/// The value as a string.
/// </summary>
/// <inheritdoc cref="IExtendedValueUnit.ValueString"/>
[JsonProperty(Order = 3)]
public string ValueString { get; [UsedImplicitly] set; }

/// <summary>
/// The type of the value, e.g. "decimal".
/// </summary>
/// <inheritdoc cref="IExtendedValueUnit.ValueType"/>
[JsonProperty(Order = 4)]
public string ValueType { get; [UsedImplicitly] set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public override IComparable ReadJson([NotNull] JsonReader reader, Type objectTyp
return token.ToObject<IComparable>(localSerializer);
}

return ConvertValueUnit(valueUnit) as IComparable;
return BaseConverter.ConvertValueUnit(valueUnit) as IComparable;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ public override void WriteJson([NotNull] JsonWriter writer, IQuantity value, [No
return;
}

var valueUnit = ConvertIQuantity(value);
var valueUnit = (ValueUnit) BaseConverter.ConvertIQuantity(value, CreateValueUnit, CreateExtendedValueUnit);

serializer.Serialize(writer, valueUnit);
}



/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
Expand Down Expand Up @@ -64,7 +66,7 @@ public override IQuantity ReadJson([NotNull] JsonReader reader, Type objectType,

var valueUnit = ReadValueUnit(token);

return ConvertValueUnit(valueUnit);
return BaseConverter.ConvertValueUnit(valueUnit);
}
}
}
53 changes: 53 additions & 0 deletions UnitsNet.Serialization.SystemTextJson.Tests/ChuckerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed under MIT No Attribution, see LICENSE file at the root.
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.

using UnitsNet.Serialization.SystemTextJson.Tests.Infrastructure;
using Xunit;

namespace UnitsNet.Serialization.SystemTextJson.Tests
{
public class ChuckerTest : UnitsNetJsonBaseTest
{
[Fact]
public void CellularDataUsage_CanDeserialize()
{
var expected = new CellularDataUsage(
Remaining: Information.FromGibibits(16),
Incremental: null,
Total: Information.FromKibibits(32));

var json = SerializeObject(expected);

var actual = DeserializeObject<CellularDataUsage>(json);

Assert.Equal(expected.Remaining, actual.Remaining);
Assert.Equal(expected.Incremental, actual.Incremental);
Assert.Equal(expected.Total, actual.Total);


}

[Fact]
public void UnitsNet905_CanDeserialize()
{
var expected = new UnitsNet905
{
DataUsage = UnitsNet.Information.FromGibibytes(15)
};

var json = SerializeObject(expected);

var actual = DeserializeObject<UnitsNet905>(json);

Assert.Equal(expected.DataUsage, actual.DataUsage);
}
}

public class UnitsNet905
{
public Information DataUsage { get; set; }
}

public record CellularDataUsage(Information Remaining, Information? Incremental, Information Total);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Text.Json;
using UnitsNet.Serialization.SystemTextJson.Tests.Infrastructure;
using UnitsNet.Tests.CustomQuantities;
using Xunit;

namespace UnitsNet.Serialization.SystemTextJson.Tests.CustomQuantities
{
public class HowMuchTests
{
[Fact]
public static void SerializeAndDeserializeCreatesSameObjectForIQuantity()
{
var jsonSerializerSettings = new JsonSerializerOptions() { WriteIndented = true };
var quantityConverterFactory = new UnitsNetIQuantityJsonConverterFactory();
quantityConverterFactory.RegisterCustomType(typeof(HowMuch), typeof(HowMuchUnit));
jsonSerializerSettings.Converters.Add(quantityConverterFactory);

var quantity = new HowMuch(12.34, HowMuchUnit.ATon);

var serializedQuantity = JsonSerializer.Serialize(quantity, jsonSerializerSettings);

var deserializedQuantity = JsonSerializer.Deserialize<HowMuch>(serializedQuantity, jsonSerializerSettings);
Assert.Equal(quantity, deserializedQuantity);
}
}
}
Loading