Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to deserialize an object with a byte array property using Json.NET 8.0.1

After upgrading a code base to use Json.NET 8.0.1, some deserialization stumbles. Using Json.NET 7.0.1 everything works fine. Apparently it is the deserialization of a property of type byte[] that causes the problem. If I remove the byte[] property it works fine. I can reproduce the behavior using this simple console application:

internal class Program
{
    private static void Main(string[] args)
    {
        Dictionary<string, Account> accounts;
        var jsonSerializerSettings = new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Objects,
            TypeNameAssemblyFormat = FormatterAssemblyStyle.Simple
        };

        using (var streamReader = new StreamReader("accounts.json"))
        {
            var json = streamReader.ReadToEnd();
            accounts = JsonConvert.DeserializeObject<Dictionary<string, Account>>(json, jsonSerializerSettings);
        }

        foreach (var account in accounts)
        {
            Debug.WriteLine(account.Value.Name);
        }
    }
}

internal class Account
{
    public string Id { get; set; }

    public string Name { get; set; }

    public byte[] EncryptedPassword { get; set; }
}

The accounts.json file looks like this:

{
    "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[ConsoleApplication1.Account, ConsoleApplication1]], mscorlib",
    "lars.michael": {
        "$type": "ConsoleApplication1.Account, ConsoleApplication1",
        "EncryptedPassword": {
            "$type": "System.Byte[], mscorlib",
            "$value": "cGFzc3dvcmQ="
        },
        "Name": "Lars Michael",
        "Id": "lars.michael"
    },
    "john.doe": {
        "$type": "ConsoleApplication1.Account, ConsoleApplication1",
        "EncryptedPassword": {
            "$type": "System.Byte[], mscorlib",
            "$value": "cGFzc3dvcmQ="
        },
        "Name": "John Doe",
        "Id": "john.doe"
    }
}

Is this possibly a bug in Json.NET 8.0.1 or can I maybe solve this by tweaking the JsonSerializerSettings?

If anyone is trying to reproduce this, make sure to synchronize the assembly name in the accounts.json file with the assembly name of the console application (in this case ConsoleApplication1).

like image 846
Lars Michael Avatar asked Oct 31 '22 11:10

Lars Michael


1 Answers

Update

Fixed in change set 70120ce, which will be included in Json.NET 8.0.2.

Original Answer

Confirmed - this appears to be a regression. Consider the following simple test class:

internal class HasByteArray
{
    public byte[] EncryptedPassword { get; set; }
}

Now if I try to round-trip the class with TypeNameHandling.Objects:

    private static void TestSimple()
    {
        var test = new HasByteArray { EncryptedPassword = Convert.FromBase64String("cGFzc3dvcmQ=") };
        try
        {
            TestRoundTrip(test);
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex);
        }
    }

    private static void TestRoundTrip<T>(T item)
    {
        var jsonSerializerSettings = new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Objects,
            TypeNameAssemblyFormat = FormatterAssemblyStyle.Simple
        };

        TestRoundTrip<T>(item, jsonSerializerSettings);
    }

    private static void TestRoundTrip<T>(T item, JsonSerializerSettings jsonSerializerSettings)
    {
        var json = JsonConvert.SerializeObject(item, Formatting.Indented, jsonSerializerSettings);
        Debug.WriteLine(json);

        var item2 = JsonConvert.DeserializeObject<T>(json, jsonSerializerSettings);

        var json2 = JsonConvert.SerializeObject(item2, Formatting.Indented, jsonSerializerSettings);

        Debug.WriteLine(json2);

        if (!JToken.DeepEquals(JToken.Parse(json), JToken.Parse(json2)))
            throw new InvalidOperationException("Round Trip Failed");
    }

I get the following exception:

Newtonsoft.Json.JsonSerializationException: Additional text found in JSON string after finishing deserializing object.
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) in C:\Development\Releases\Json\Working\Newtonsoft.Json\Working-Signed\Src\Newtonsoft.Json\Serialization\JsonSerializerInternalReader.cs:line 196
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) in C:\Development\Releases\Json\Working\Newtonsoft.Json\Working-Signed\Src\Newtonsoft.Json\JsonSerializer.cs:line 823
   at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType) in C:\Development\Releases\Json\Working\Newtonsoft.Json\Working-Signed\Src\Newtonsoft.Json\JsonSerializer.cs:line 802
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings) in C:\Development\Releases\Json\Working\Newtonsoft.Json\Working-Signed\Src\Newtonsoft.Json\JsonConvert.cs:line 863
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings) in C:\Development\Releases\Json\Working\Newtonsoft.Json\Working-Signed\Src\Newtonsoft.Json\JsonConvert.cs:line 820
   at Question34654184.TestClass.TestRoundTrip[T](T item, JsonSerializerSettings jsonSerializerSettings)
   at Question34654184.TestClass.TestRoundTrip[T](T item)
   at Question34654184.TestClass.TestSimple()

The exception does not occur in Json 7.0. You should report an issue.

In the meantime, you can use the following converter to work around the problem:

public class ByteArrayConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(byte[]);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var token = JToken.Load(reader);
        if (token == null)
            return null;
        switch (token.Type)
        {
            case JTokenType.Null:
                return null;
            case JTokenType.String:
                return Convert.FromBase64String((string)token);
            case JTokenType.Object:
                {
                    var value = (string)token["$value"];
                    return value == null ? null : Convert.FromBase64String(value);
                }
            default:
                throw new JsonSerializationException("Unknown byte array format");
        }
    }

    public override bool CanWrite { get { return false; } } // Use the default implementation for serialization, which is not broken.

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

With settings

            var jsonSerializerSettings = new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Objects,
                TypeNameAssemblyFormat = FormatterAssemblyStyle.Simple,
                Converters = new [] { new ByteArrayConverter() },
            };
like image 87
dbc Avatar answered Nov 10 '22 05:11

dbc