Changes to deserialization between V4.5.4 and V5.8?

Hello,

I am upgrading a project from V4.5.4 to the latest version (V5.8) and I have encountered a change in the behavior of JsonSerializer.DeserializeFromString with a type of object.

Some context: a few years ago one of our developers had the bad idea to use a Dictionary<string, object> in one of their DTO to return multiple data types. A custom serialization/deserialization was written to handle this Dictionary<string, object>: value types (other than string) are using a custom format and everything else is left to ServiceStack to figure out how to deserialize.

A few years ago we noticed that using a Dictionary<string, object> wasn’t a great idea and we started to manually serialize complex types as string (JSON) in the dictionary instead of putting the object itself in the dictionary. This left the serialization up to the user of the dictionary.

The issue we have is with strings that contains JSON serialized values. It looks like the new JsUtils implementation from ~V5.1 is treating the JSON escaped string as a real object instead of a string. An exception is throw and the dictionary is set to null.

From that StackOverflow post, I found that setting Config.UseJsObject = false; causes the deserialization to have the same behavior as V4.5.4 but it would be great to leave it enabled to use the new JsUtils implementation.

Do you have any suggestion on how to workaround this issue or is your suggestion to simply turn of UseJsObject ?

Thanks,
Alex

You will find below a sample program to replicate the issue. Sorry for the wall of text, I was not able to create a functional Gist (ServiceStack.JS was not available)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ServiceStack;
using ServiceStack.Text;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            string jsonData = "{\r\n  \"result\": [\r\n    {\r\n      \"id\": \"123\",\r\n      \"created\": \"2020-02-13T20:42:18.4560000Z\",\r\n      \"createdBy\": \"Some guy\",\r\n      \"lastModified\": \"2020-02-13T20:42:56.6920000Z\",\r\n      \"lastModifiedBy\": \"Some guy\",\r\n      \"bagOfHolding\": {\r\n   \"Booly\": { \"__type\": \"BoolWrapper\", \"Value\": true }, \"ComplexExample\": { \"__type\": \"ComplexObject\", \"Value\": \"XYZ\" },     \"IsSuccess\": \"some string value\",\r\n        \"BookingResults\": \"{\\\"quote\\\":{\\\"quoteId\\\":\\\"123\\\",\\\"fulfillmentCarrierId\\\":\\\"234\\\",\\\"cost\\\":21.35,\\\"currency\\\":\\\"CAD\\\",\\\"fulfillmentCarrierName\\\":\\\"Canada Post\\\",\\\"fulfillmentCarrierService\\\":\\\"canada_post_expedited_parcel\\\",\\\"fulfillmentCarrierServiceDescription\\\":\\\"Expedited Parcel\\\",\\\"fulfillmentCarrierServiceOther\\\":\\\"\\\",\\\"fulfillmentCarrierDeliveryDurationDescription\\\":\\\"\\\",\\\"fulfillmentCarrierImages\\\":[{\\\"size\\\":75,\\\"url\\\":\\\"https://\\\"},{\\\"size\\\":200,\\\"url\\\":\\\"https://\\\"}],\\\"expectedDeliveryDate\\\":\\\"2020-02-14T20:42:53.1303375Z\\\",\\\"isReturn\\\":false},\\\"confirmation\\\":{\\\"confirmationId\\\":\\\"3e9fdc6e8d7b40fb89afc87d0fc57e0b\\\",\\\"quoteId\\\":\\\"345\\\",\\\"fulfillmentCarrierId\\\":\\\"456\\\",\\\"documents\\\":[{\\\"quoteDocumentId\\\":\\\"567\\\",\\\"documentFormat\\\":\\\"PDF\\\",\\\"quoteId\\\":\\\"678\\\",\\\"trackingNumber\\\":\\\"123456789012\\\",\\\"trackingUrl\\\":\\\"https\\\",\\\"labelUrl\\\":\\\"https://IG42A\\\"}]}}\"\r\n      }\r\n    }\r\n  ]\r\n}";
            var typeFinder = JsConfig.TypeFinder;
            JsConfig.TypeFinder = typeName => TypeFinder(typeName, typeFinder);
            JsConfig.TextCase = TextCase.CamelCase;
            JsConfig<BagOfHolding>.RawDeserializeFn = BagOfHolding.DeserializeFn;

            // you can comment out this line to have the BagOfHolding correctly deserialized (same behavior as V4.5.4)
            JS.Configure(); // replicating what 'Config.UseJsObject = true' is doing behind the scene

            var content = JsonSerializer.DeserializeFromString<ResponseObj>(jsonData);
            if (content.Result.First().BagOfHolding == null)
            {
                throw new Exception("BagOfHolding was not deserialized!");
            }
            Console.WriteLine(content.Dump());
        }
        private static Type TypeFinder(string typeName, Func<string, Type> typeFinder)
        {
            Console.WriteLine($"Looking for type {typeName}");
            var type = typeof(Program).Assembly.GetTypes().FirstOrDefault(x => x.Name == typeName);

            return type ?? typeFinder(typeName);
        }
    }

    class ResponseObj
    {
        public List<ResultItem> Result { get; set; }
    }

    public class BagOfHolding : Dictionary<string, object>
    {
        /*
          Once serialized to JSON, our BagOfHolding has a format similar to this:
              {
                "PropName1": { "__type": "BoolWrapper", "Value": true },
                "PropName2": { "__type": "IntWrapper", "Value": 123 },
                "PropName3": { "__type": "ComplexObject", "Value": "XYZ" },
                "PropName4": "string are not wrapped for legacy/compatibility reasons"
              }
            We have a custom format for value type which uses the same __type 'marker' which our TypeFinder understands.
            We parse the raw string and for each property we check if it is a value type and if it is not we call 
            DeserializeFromString with a type of object to let ServiceStack handle the __type property by itself
         */

        public static BagOfHolding DeserializeFn(string rawStringValue)
        {
            if (String.IsNullOrEmpty(rawStringValue))
            {
                return null;
            }

            var pb = new BagOfHolding();

            var rawValues = JsonObject.Parse(rawStringValue);

            foreach (var kvp in rawValues)
            {
                if (HasBoolType(kvp))
                {
                    pb.Add(kvp.Key, JsonSerializer.DeserializeFromString(kvp.Value, typeof(BoolWrapper)));
                }
                // other cases removed for simplicity
                else
                {
                    // this will throw an exception that is hidden by the deserialization process
                    pb.Add(kvp.Key, JsonSerializer.DeserializeFromString(kvp.Value, typeof(object)));
                }
            }

            return pb;
        }

        private static bool HasBoolType(KeyValuePair<string, string> kvp)
        {
            // logic was simplified for this example
            var value = kvp.Value;
            if (!string.IsNullOrWhiteSpace(value) && value.Contains("BoolWrapper") && (value.StartsWith("{") && value.EndsWith("}") || (value.StartsWith("[") && value.EndsWith("]"))) && (value.Contains(@"""__type""") && !value.Contains(@"\""__type\""")))
            {
                return true;
            }

            return false;
        }
    }

    class ResultItem
    {
        public Guid Id { get; set; }
        public DateTime Created { get; set; }
        public string CreatedBy { get; set; }
        public DateTime LastModified { get; set; }
        public string LastModifiedBy { get; set; }
        public BagOfHolding BagOfHolding { get; set; }
    }

    class BoolWrapper // sample of a value type wrapper
    {
        public bool Value { get; set; }
    }

    class ComplexObject // simulate a complex object
    {
        public string Value { get; set; }
    }

}

The long term solution is to avoid using unknown Types like object in your Service Contracts.

You’d UseJsObject if you want to capture arbitrary JSON in an object Type, e.g. a scalar Type like a string or double, or complex types like an Array or Object dictionary. It doesn’t let you do magic translation into any C# Type, it’s strictly for capturing the JSON data structure into the appropriate C# data structures.

So you can’t use it here because you’re relying on the Runtime Types features of ServiceStack.Text.

Thanks for the info, we’ll set UseJsObject to false for our project.

Is it possible to set UseJsConfig in a JsConfig scope?

No behind the scenes it’s setting a singleton static config property that replaces ServiceStack.Text’s built-in Object deserializer with the JS Utils implementation, essentially it’s just doing:

JsonTypeSerializer.Instance.ObjectDeserializer = JSON.parseSpan;
1 Like