2

I have an API that returns a JSON object from a MongoDB in which one of the properties is an "open-ended" document, meaning it can be any valid JSON for that property. I don't know what the names of the properties are ahead of time, they can be any string. I only know that this particular property needs to be serialized exactly how it is stored in the database. So if the property name that was originally stored was "Someproperty", the serialized response in JSON needs to be "Someproperty", NOT "someProperty".

We have this configuration:

ContractResolver = new CamelCasePropertyNamesContractResolver();

in our CustomJsonSerializer, but it is messing with the formatting of the response when returning the "open ended" JSON. It is camel-casing all of these properties when in fact we want the response to be exactly how they are stored in MongoDB (BSON). I know the values are maintaining their case when storing/retrieving via the database, so that is not the issue.

How can I tell JSON.net to essentially bypass the CamelCasePropertyNameResolver for all of the child properties of a particular data point?

EDIT: Just to give a bit more info, and share what I have already tried:

I thought about overriding the PropertyNameResolver like so:

protected override string ResolvePropertyName(string propertyName)
{
      if (propertyName.ToLower().Equals("somedocument"))
      {
                return propertyName;
      }
      else return base.ResolvePropertyName(propertyName);
}

However, if I have a JSON structure like this:

{
   "Name" : "MyObject",
   "DateCreated" : "11/14/2016",
   "SomeDocument" : 
   {
      "MyFirstProperty" : "foo",
      "mysecondPROPERTY" : "bar",
      "another_random_subdoc" : 
      {
         "evenmoredata" : "morestuff"
      }
   }
}

then I would need all of the properties any child properties' names to remain exactly as is. The above override I posted would (I believe) only ignore on an exact match to "somedocument", and would still camelcase all of the child properties.

Josh
  • 451
  • 8
  • 22
  • Could the same type appear above and below the relevant data point, and require different casing in different contexts? – dbc Nov 14 '16 at 22:13
  • everything else besides the "SomeDocument" property can be camel-cased. I don't care about DateCreated, for example. That can respond as "dateCreated". In fact, "SomeDocument" (the root level property name) can even be camel cased, it's the "MyFirstProperty", "mysecondPROPERTY" that need to maintain their original casing. – Josh Nov 15 '16 at 18:20

2 Answers2

2

What you can do is, for the property in question, create a custom JsonConverter that serializes the property value in question using a different JsonSerializer created with a different contract resolver, like so:

public class AlternateContractResolverConverter : JsonConverter
{
    [ThreadStatic]
    static Stack<Type> contractResolverTypeStack;

    static Stack<Type> ContractResolverTypeStack { get { return contractResolverTypeStack = (contractResolverTypeStack ?? new Stack<Type>()); } }

    readonly IContractResolver resolver;

    JsonSerializerSettings ExtractAndOverrideSettings(JsonSerializer serializer)
    {
        var settings = serializer.ExtractSettings();
        settings.ContractResolver = resolver;
        settings.CheckAdditionalContent = false;
        if (settings.PreserveReferencesHandling != PreserveReferencesHandling.None)
        {
            // Log an error throw an exception?
            Debug.WriteLine(string.Format("PreserveReferencesHandling.{0} not supported", serializer.PreserveReferencesHandling));
        }
        return settings;
    }

    public AlternateContractResolverConverter(Type resolverType)
    {
        if (resolverType == null)
            throw new ArgumentNullException("resolverType");
        resolver = (IContractResolver)Activator.CreateInstance(resolverType);
        if (resolver == null)
            throw new ArgumentNullException(string.Format("Resolver type {0} not found", resolverType));
    }

    public override bool CanRead { get { return ContractResolverTypeStack.Count == 0 || ContractResolverTypeStack.Peek() != resolver.GetType(); } }
    public override bool CanWrite { get { return ContractResolverTypeStack.Count == 0 || ContractResolverTypeStack.Peek() != resolver.GetType(); } }

    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException("This contract resolver is intended to be applied directly with [JsonConverter(typeof(AlternateContractResolverConverter), typeof(SomeContractResolver))] or [JsonProperty(ItemConverterType = typeof(AlternateContractResolverConverter), ItemConverterParameters = ...)]");
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        using (ContractResolverTypeStack.PushUsing(resolver.GetType()))
            return JsonSerializer.CreateDefault(ExtractAndOverrideSettings(serializer)).Deserialize(reader, objectType);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        using (ContractResolverTypeStack.PushUsing(resolver.GetType()))
            JsonSerializer.CreateDefault(ExtractAndOverrideSettings(serializer)).Serialize(writer, value);
    }
}

internal static class JsonSerializerExtensions
{
    public static JsonSerializerSettings ExtractSettings(this JsonSerializer serializer)
    {
        // There is no built-in API to extract the settings from a JsonSerializer back into JsonSerializerSettings,
        // so we have to fake it here.
        if (serializer == null)
            throw new ArgumentNullException("serializer");
        var settings = new JsonSerializerSettings
        {
            CheckAdditionalContent = serializer.CheckAdditionalContent,
            ConstructorHandling = serializer.ConstructorHandling,
            ContractResolver = serializer.ContractResolver,
            Converters = serializer.Converters,
            Context = serializer.Context,
            Culture = serializer.Culture,
            DateFormatHandling = serializer.DateFormatHandling,
            DateFormatString = serializer.DateFormatString,
            DateParseHandling = serializer.DateParseHandling,
            DateTimeZoneHandling = serializer.DateTimeZoneHandling,
            DefaultValueHandling = serializer.DefaultValueHandling,
            EqualityComparer = serializer.EqualityComparer,
            // No Get access to the error event, so it cannot be copied.
            // Error = += serializer.Error
            FloatFormatHandling = serializer.FloatFormatHandling,
            FloatParseHandling = serializer.FloatParseHandling,
            Formatting = serializer.Formatting,
            MaxDepth = serializer.MaxDepth,
            MetadataPropertyHandling = serializer.MetadataPropertyHandling,
            MissingMemberHandling = serializer.MissingMemberHandling,
            NullValueHandling = serializer.NullValueHandling,
            ObjectCreationHandling = serializer.ObjectCreationHandling,
            ReferenceLoopHandling = serializer.ReferenceLoopHandling,
            // Copying the reference resolver doesn't work in the default case, since the
            // actual BidirectionalDictionary<string, object> mappings are held in the 
            // JsonSerializerInternalBase.
            // See https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/DefaultReferenceResolver.cs
            ReferenceResolverProvider = () => serializer.ReferenceResolver,
            PreserveReferencesHandling = serializer.PreserveReferencesHandling,
            StringEscapeHandling = serializer.StringEscapeHandling,
            TraceWriter = serializer.TraceWriter,
            TypeNameHandling = serializer.TypeNameHandling,
            // Changes in Json.NET 10.0.1
            //TypeNameAssemblyFormat was obsoleted and replaced with TypeNameAssemblyFormatHandling in Json.NET 10.0.1
            //TypeNameAssemblyFormat = serializer.TypeNameAssemblyFormat,
            TypeNameAssemblyFormatHandling = serializer.TypeNameAssemblyFormatHandling,
            //Binder was obsoleted and replaced with SerializationBinder in Json.NET 10.0.1
            //Binder = serializer.Binder,
            SerializationBinder = serializer.SerializationBinder,
        };
        return settings;
    }
}

public static class StackExtensions
{
    public struct PushValue<T> : IDisposable
    {
        readonly Stack<T> stack;

        public PushValue(T value, Stack<T> stack)
        {
            this.stack = stack;
            stack.Push(value);
        }

        // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
        public void Dispose()
        {
            if (stack != null)
                stack.Pop();
        }
    }

    public static PushValue<T> PushUsing<T>(this Stack<T> stack, T value)
    {
        if (stack == null)
            throw new ArgumentNullException();
        return new PushValue<T>(value, stack);
    }
}

Then use it like so:

public class RootObject
{
    public string Name { get; set; }
    public DateTime DateCreated { get; set; }

    [JsonProperty(NamingStrategyType = typeof(DefaultNamingStrategy))]
    [JsonConverter(typeof(AlternateContractResolverConverter), typeof(DefaultContractResolver))]
    public SomeDocument SomeDocument { get; set; }
}

public class SomeDocument
{
    public string MyFirstProperty { get; set; }
    public string mysecondPROPERTY { get; set; }
    public AnotherRandomSubdoc another_random_subdoc { get; set; }
}

public class AnotherRandomSubdoc
{
    public string evenmoredata { get; set; }
    public DateTime DateCreated { get; set; }
}

(Here I am assuming you want the "SomeDocument" property name to be serialized verbatim, even though it wasn't entirely clear from your question. To do that, I'm using JsonPropertyAttribute.NamingStrategyType from Json.NET 9.0.1. If you're using an earlier version, you'll need to set the property name explicitly.)

Then the resulting JSON will be:

{
  "name": "Question 40597532",
  "dateCreated": "2016-11-14T05:00:00Z",
  "SomeDocument": {
    "MyFirstProperty": "my first property",
    "mysecondPROPERTY": "my second property",
    "another_random_subdoc": {
      "evenmoredata": "even more data",
      "DateCreated": "2016-11-14T05:00:00Z"
    }
  }
}

Note that this solution does NOT work well with preserving object references. If you need them to work together, you may need to consider a stack-based approach similar to the one from Json.NET serialize by depth and attribute

Demo fiddle here.

Incidentally, have you considered storing this JSON as a raw string literal, as in the answer to this question?

dbc
  • 104,963
  • 20
  • 228
  • 340
1

I think you should look at this backwards.

Instead of trying to NOT touch the properties you don't know, let that be the default behavior and touch the ones you DO know.

In other words, don't use the CamelCasePropertyNamesContractResolver. Deal with the properties you know appropriately and let the other ones pass through transparently.

Dovydas Šopa
  • 2,282
  • 8
  • 26
  • 34
JuanR
  • 7,405
  • 1
  • 19
  • 30
  • Unfortunately, this is the only exception, the CamelCased behaviour is what we want for every other data point that is returned by the API (there are many others). It is only this specific property and it's sub properties that need to adhere to the non-camel-cased convention. – Josh Nov 14 '16 at 21:18
  • I see. In any event, you simply cannot possibly account for something you don't know about. What does the resulting deserialized class look like? It would help a bit if I understood how you are using the resulting deserialized object. – JuanR Nov 14 '16 at 21:22