TextCase.SnakeCase deserialization issue

There is some issue while deserializing json with snake_case notation.

Just found it in the 5.13.2 version.

How to reproduce:

    class Dto
    {
        public int? Property { get; set; }
        public int? AnotherProperty { get; set; }
    }

    // Program.Main()
    JsConfig.Init(new Config
    {
        TextCase = TextCase.SnakeCase
    });
    var dto = new Dto { Property = 1, AnotherProperty = 1 };
    var json = dto.ToJson(); // json: {"property": 1, "another_property": 1}
    var anotherDto = json.FromJson<Dto>(); // anotherDto.AnotherProperty is null

The serialization looks good and the property names with capital letters in the middle got “snake_cased”, however while trying to deserialize back, the very same property has no value assigned.

Use:

JsConfig.Init(new ServiceStack.Text.Config {
    TextCase = TextCase.SnakeCase,
    PropertyConvention = PropertyConvention.Lenient,
});
1 Like

Thanks! I even bothered to clone the ServiceStack.Text repo to check why is that so, and found the part responsible for parsing:

        internal static TypeAccessor Get(this KeyValuePair<string, TypeAccessor>[] accessors, ReadOnlySpan<char> propertyName, bool lenient)
        {
            var testValue = FindPropertyAccessor(accessors, propertyName);
            if (testValue != null) 
                return testValue;

            if (lenient)
                return FindPropertyAccessor(accessors, 
                    propertyName.ToString().Replace("-", string.Empty).Replace("_", string.Empty).AsSpan());
            
            return null;
        }

Isn’t that wrong behavior? I assumed, that setting the TextCase.SnakeCase in JsConfig is valid for both: serializing and deserializing. However, the code clearly says the TextCase is only utilized during serialization and in one place during deserialization (when the type is enum). The PropertyConvention on the other hand, is only used during deserialization.

Don’t take me wrong. I’m totally fine having an extra line of code, as you suggested:

JsConfig.Init(new ServiceStack.Text.Config {
    TextCase = TextCase.SnakeCase,
    PropertyConvention = PropertyConvention.Lenient,
});

I actually workarounded the issue by setting DataMember(Name = "another_property") attributes.

I just think, if you already provide such a customization tool for DTO parsing (which is, to be clear, AWESOME! :heart: ) this is unintuitive and TextCase should also be respected during deserialization.

Yeah it was only used for serialization, I’ve changed it to be used during deserialization from the latest v5.13.3 that’s now available on MyGet where you can just do:

JsConfig.Init(new ServiceStack.Text.Config {
    TextCase = TextCase.SnakeCase,
});
2 Likes

Is there a way to specify different serialization locally? Because when calling different 3rd party APIs, they have different needs. For the FromJson or similar method.

Some customization is available using custom config scopes.

1 Like

Thank you, that works beautifully.

1 Like

I regret my answer. It didn’t work so beautifully after all.

Even with the using(new JSConfig.With(..)) { /* api calls here */ } there’s a big chance of configuration leak.

At least this is my experience. It’s just too easy to make a mistake (human error). I’ve had lots of trouble with serialization/desalinization issues earlier, so when I see this it gives me goosebumps.

A few examples:

First the long form with brackets, which deserializes correctly.

using (JsConfig.With(new Config
                   {
                       TextCase = TextCase.CamelCase,
                       PropertyConvention = PropertyConvention.Lenient
                   })) 
{
   response.FromJson<MyType>()
}

Tempting to try the syntax without the brackets:

using (JsConfig.With(new Config
                   {
                       TextCase = TextCase.SnakeCase,
                       PropertyConvention = PropertyConvention.Lenient
                   }));
response.FromJson<MyType>()

This short form of using usually works. But not in this case (don’t know why).

So back to the long form w. brackets, but it’s too big to have everywhere, so let’s keep the config in a variable to avoid some repetition:

// kept globally in my class
var theConfig = JsConfig.With(new Config
{
   TextCase = TextCase.CamelCase,
   PropertyConvention = PropertyConvention.Lenient
 }));

// then many places:
using(theConfig) {
    response.FromJson<MyType>()
}

Deserialization works, but now I discovered snake-case in my database (stored by OrmLite). So the config has leaked into the global config used by OrmLite (and more).

I’m not reporting this as a ServiceStack bug – it’s probably a human error by myself – but it shows how easily the JSConfig can leak to places you don’t want, just by programming normally.

Recommendation
So this is what I’m using now – a separate JSON parser for stuff not-ServiceStack, to avoid any mixup. Here I’m using Newtonsoft’s:

// keep a class-global instance of this:
JsonSerializerSettings mySettings = new JsonSerializerSettings {
        ContractResolver = new DefaultContractResolver() {
            NamingStrategy = new SnakeCaseNamingStrategy()
        }
};
// then wherever:
var parsedResult = JsonConvert.DeserializeObject<MyType>(responseBody, mySettings );

You shouldn’t keep a global reference to a to a config scope, it’s only for executing a custom configuration within a scoped logic block executing on a single thread. It should always be used within a using block to ensure the scope is limited to a logic block. Failing to release the scope will result in that worker thread continuing to use that configuration for other uses.

If you want to reuse a configuration, don’t reuse a scoped configuration, instead create a factory that you can use to create scoped configurations, e.g:

// custom config factory function
var theConfig = () => JsConfig.With(new Config
{
    TextCase = TextCase.CamelCase,
    PropertyConvention = PropertyConvention.Lenient
});

// then many places:
using (theConfig())
{
    
}

Or you can reuse the same configuration, but not the scope:

// custom config factory function
var theConfig = new Config {
    TextCase = TextCase.CamelCase,
    PropertyConvention = PropertyConvention.Lenient
};

// then many places:
using (JsConfig.With(theConfig))
{
    
}

But yeah using a different serializer will avoid any leaking configuration.