JamesNK / Newtonsoft.Json

Json.NET is a popular high-performance JSON framework for .NET
https://www.newtonsoft.com/json
MIT License
10.8k stars 3.26k forks source link

Support conversion of types which have implicit conversion operators + TypeConverter #1411

Open gcampbell-epiq opened 7 years ago

gcampbell-epiq commented 7 years ago

I have a type that I'd like deserialized from a json string to another type. This example demonstrates the issue.


void Main()
{
    var thing = new StringLikeThing("blah");
    var serialized = JsonConvert.SerializeObject(thing);
    if (serialized != "\"blah\"") // Passes!
    {
        throw new Exception("thing was not serialized as a json string");
    }

    if (!(JsonConvert.DeserializeObject<StringLikeThing>(serialized) is StringLikeThing)) // Fails!
    {
        throw new Exception("json string was deserialized as StringLikeThing");
    }
}

[TypeConverter(typeof(StringConverter))]
public class StringLikeThing
{
    public string Name { get; private set; }
    public StringLikeThing(string name)
    {
        Name = name;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        return (obj as StringLikeThing)?.Name == Name;
    }

    public override string ToString()
    {
        return Name;
    }

    public static explicit operator StringLikeThing(string name) { return new StringLikeThing(name); }
    public static explicit operator string(StringLikeThing op) { return op?.Name; }
}

The following is thrown from the call to DeserializeObject.

Unable to cast object of type 'System.String' to type 'StringLikeThing'.

   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value)   at UserQuery.Main()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

I know there are workarounds. This is a matter of API cosmetics. Thanks.

YarekTyshchenko commented 3 years ago

What are the workarounds?

wplong11 commented 3 years ago

@YarekTyshchenko This sample works well

[TestMethod, AutoDomainData]
public void Sut_is_deserialized_correctly()
{
    // Arrange
    string sourceJson = "{ id: \"112d761b-abb6-4c80-823e-8f6d28df813b:roomRateIda870fc97-d669-4382-9677-59ee0a269a30\" }";

    // Act
    RoomRateSource source = JsonConvert.DeserializeObject<RoomRateSource>(sourceJson);

    // Assert
    Assert.AreEqual(source.Id.SupplierId, Guid.Parse("112d761b-abb6-4c80-823e-8f6d28df813b"));
    Assert.AreEqual(source.Id.RoomRateId, "roomRateIda870fc97-d669-4382-9677-59ee0a269a30");
}

public sealed class RoomRateSource
{
    public RoomRateSource(
        RoomRateSourceId id)
    {
        Id = id;
    }

    public RoomRateSourceId Id { get; }
}

public struct RoomRateSourceId
{
    private const string _discriminator = ":";

    public RoomRateSourceId(
        Guid supplierId,
        string roomRateId)
    {
        SupplierId = supplierId;
        RoomRateId = roomRateId;
    }

    public Guid SupplierId { get; }

    public string RoomRateId { get; }

    public override string ToString() =>
        $"{SupplierId}{_discriminator}{RoomRateId}";

    public static implicit operator RoomRateSourceId(string sourceId)
    {
        if (string.IsNullOrWhiteSpace(sourceId))
        {
            throw new ArgumentException($"'{nameof(sourceId)}' cannot be null or whitespace", nameof(sourceId));
        }

        string[] segments = sourceId.Split(_discriminator, 2, StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length < 2)
        {
            throw new ArgumentException($"The form of '{nameof(sourceId)}' is invalid. It should be form of `{{supplierId}}{_discriminator}{{roomRateId}}`", nameof(sourceId));
        }

        return new RoomRateSourceId(
            supplierId: Guid.Parse(segments[0]),
            roomRateId: segments[1]);
    }

    public static implicit operator string(RoomRateSourceId id) =>
        id.ToString();
}