Skip to content

JSON deserialization of quantities that use decimal internally may fail #847

Closed
@bitbonk

Description

@bitbonk

Describe the bug
When serializing and deserializing quantities that use the decimal type for storage interally, JSON deserialization using the converters from UnitsNet.Serialization.JsonNet may fail with an OverflowException if the value is too big or too large.

To Reproduce
Steps to reproduce the behavior (just an example):

  1. Add nuget UnitsNet 4.72.0, UnitsNet.Serialization.JsonNet 4.2.0, Xunit, and FluentAssertions to a .NET Core 3.1 class library project
  2. Build and run the test code below.
  3. See 6 failing tests.
    using System.Collections.Generic;
    using System.Linq;
    using FluentAssertions;
    using Newtonsoft.Json;
    using UnitsNet;
    using UnitsNet.Serialization.JsonNet;
    using Xunit;

    public class QuantitySerializationTests
    {
        public static IEnumerable<object[]> QuantityZeros { get; } =
            Quantity.Infos.Select(
                info => new object[]
                {
                    (IQuantity)info.Zero
                });
        
        public static IEnumerable<object[]> QuantityMinValues { get; } =
            Quantity.Infos.Select(
                info => new object[]
                {
                    (IQuantity)info.ValueType.GetProperty("MinValue").GetValue(null)
                });
        
        public static IEnumerable<object[]> QuantityMaxValues { get; } =
            Quantity.Infos.Select(
                info => new object[]
                {
                    (IQuantity)info.ValueType.GetProperty("MaxValue").GetValue(null)
                });

        [Theory]
        [MemberData(nameof(QuantityZeros))]
        public void Zero_IsMessagePackSerializable(IQuantity value)
        {
            var sutClone =
                JsonConvert.DeserializeObject(
                    JsonConvert.SerializeObject(value, Settings),
                    value.GetType(), Settings);

            sutClone.Should().NotBeSameAs(value);
            sutClone.Should().BeEquivalentTo(value, o => o.RespectingRuntimeTypes());
        }

        [Theory]
        [MemberData(nameof(QuantityMinValues))]
        public void MinValue_IsMessagePackSerializable(IQuantity value)
        {
            var sutClone =
                JsonConvert.DeserializeObject(
                    JsonConvert.SerializeObject(value, Settings),
                    value.GetType(), Settings);

            sutClone.Should().NotBeSameAs(value);
            sutClone.Should().BeEquivalentTo(value, o => o.RespectingRuntimeTypes());
        }

        [Theory]
        [MemberData(nameof(QuantityMaxValues))]
        public void MaxValue_IsMessagePackSerializable(IQuantity value)
        {
            var sutClone =
                JsonConvert.DeserializeObject(
                    JsonConvert.SerializeObject(value, Settings),
                    value.GetType(), Settings);

            sutClone.Should().NotBeSameAs(value);
            sutClone.Should().BeEquivalentTo(value, o => o.RespectingRuntimeTypes());
        }
        
        private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Auto,
            Converters =
            {
                new UnitsNetIQuantityJsonConverter(),
                new UnitsNetIComparableJsonConverter()
            }
        };
    }

Example failed test result:

Tests.QuantitySerializationTests.MaxValue_IsMessagePackSerializable.MaxValue_IsMessagePackSerializable(value: 7,92e+28 W)

System.OverflowException: Value was either too large or too small for a Decimal.

System.OverflowException
Value was either too large or too small for a Decimal.
at System.Number.ThrowOverflowException(TypeCode type)
at System.Decimal.DecCalc.VarDecFromR8(Double input, DecCalc& result)
at System.Decimal.op_Explicit(Double value)
at UnitsNet.QuantityValue.op_Explicit(QuantityValue number)
at UnitsNet.Power.From(QuantityValue value, PowerUnit fromUnit)
at UnitsNet.Quantity.TryFrom(QuantityValue value, Enum unit, IQuantity& quantity)
at UnitsNet.Quantity.From(QuantityValue value, Enum unit)
at UnitsNet.Serialization.JsonNet.UnitsNetBaseJsonConverter1.ConvertValueUnit(ValueUnit valueUnit) at UnitsNet.Serialization.JsonNet.UnitsNetIQuantityJsonConverter.ReadJson(JsonReader reader, Type objectType, IQuantity existingValue, Boolean hasExistingValue, JsonSerializer > serializer) at Newtonsoft.Json.JsonConverter1.ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType)
at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
at Tests.QuantitySerializationTests

Expected behavior
No tests fail.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions