JSON.Net Serialize by Depth and Attribute

Json.NET serialize by depth and attribute

The basic difficulty here is that Json.NET is a contract-based serializer which creates a contract for each type to be serialized, then serializes according to the contract. No matter where a type appears in the object graph, the same contract applies. But you want to selectively include properties for a given type depending on its depth in the object graph, which conflicts with the basic "one type one contract" design and thus requires some work.

One way to accomplish what you want would be to create a JsonConverter that performs a default serialization for each object, then prunes undesired properties, along the lines of Generic method of modifying JSON before being returned to client. Note that this has problems with recursive structures such as trees, because the converter must disable itself for child nodes to avoid infinite recursion.

Another possibility would be to create a custom IContractResolver that returns a different contract for each type depending on the serialization depth. This must needs make use of serialization callbacks to track when object serialization begins and ends, since serialization depth is not made known to the contract resolver:

[System.AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true, Inherited = true)]
public class JsonIncludeAtDepthAttribute : System.Attribute
{
public JsonIncludeAtDepthAttribute()
{
}
}

public class DepthPruningContractResolver : IContractResolver
{
readonly int depth;

public DepthPruningContractResolver()
: this(0)
{
}

public DepthPruningContractResolver(int depth)
{
if (depth < 0)
throw new ArgumentOutOfRangeException("depth");
this.depth = depth;
}

[ThreadStatic]
static DepthTracker currentTracker;

static DepthTracker CurrentTracker { get { return currentTracker; } set { currentTracker = value; } }

class DepthTracker : IDisposable
{
int isDisposed;
DepthTracker oldTracker;

public DepthTracker()
{
isDisposed = 0;
oldTracker = CurrentTracker;
currentTracker = this;
}

#region IDisposable Members

public void Dispose()
{
if (0 == Interlocked.Exchange(ref isDisposed, 1))
{
CurrentTracker = oldTracker;
oldTracker = null;
}
}
#endregion

public int Depth { get; set; }
}

abstract class DepthTrackingContractResolver : DefaultContractResolver
{
static DepthTrackingContractResolver() { } // Mark type with beforefieldinit.

static SerializationCallback OnSerializing = (o, context) =>
{
if (CurrentTracker != null)
CurrentTracker.Depth++;
};

static SerializationCallback OnSerialized = (o, context) =>
{
if (CurrentTracker != null)
CurrentTracker.Depth--;
};

protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
contract.OnSerializingCallbacks.Add(OnSerializing);
contract.OnSerializedCallbacks.Add(OnSerialized);
return contract;
}
}

sealed class RootContractResolver : DepthTrackingContractResolver
{
// As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
// http://www.newtonsoft.com/json/help/html/ContractResolver.htm
// http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
// "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
static RootContractResolver instance;
static RootContractResolver() { instance = new RootContractResolver(); }
public static RootContractResolver Instance { get { return instance; } }
}

sealed class NestedContractResolver : DepthTrackingContractResolver
{
static NestedContractResolver instance;
static NestedContractResolver() { instance = new NestedContractResolver(); }
public static NestedContractResolver Instance { get { return instance; } }

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);

if (property.AttributeProvider.GetAttributes(typeof(JsonIncludeAtDepthAttribute), true).Count == 0)
{
property.Ignored = true;
}

return property;
}
}

public static IDisposable CreateTracker()
{
return new DepthTracker();
}

#region IContractResolver Members

public JsonContract ResolveContract(Type type)
{
if (CurrentTracker != null && CurrentTracker.Depth > depth)
return NestedContractResolver.Instance.ResolveContract(type);
else
return RootContractResolver.Instance.ResolveContract(type);
}

#endregion
}

Then mark your classes as follows:

class FooA
{
[JsonIncludeAtDepthAttribute]
public int SomeValueA { get; set; }

public int SomeValueB { get; set; }

public int SomeValueC { get; set; }
}

class FooB
{
public FooA FooA { get; set; }
}

And serialize as follows:

var settings = new JsonSerializerSettings { ContractResolver = new DepthPruningContractResolver(depth), Formatting = Formatting.Indented };

using (DepthPruningContractResolver.CreateTracker())
{
var jsonB = JsonConvert.SerializeObject(foob, settings);
Console.WriteLine(jsonB);

var jsonA = JsonConvert.SerializeObject(foob.FooA, settings);
Console.WriteLine(jsonA);
}

The slightly awkward CreateTracker() is needed to ensure that, in the event an exception is thrown partway through serialization, the current object depth gets reset and does not affect future calls to JsonConvert.SerializeObject().

Custom Json Serializer for deep nested objects

Your "goal" JSON is tricky to handle because the treatment of the SubDataMappers list is different depending on whether the children have a non-null DataMapperProperty or a non-empty list of SubDataMappers. In the former case, you want it rendered as an object containing one property per child DataMapper; in the latter, as an array of objects containing one DataMapper each. Also, I see you are using the Name property of the DataMapper as a key in the JSON rather than as the value of a well-known property. Given these two constraints, I think the best plan of attack is to make a JsonConverter that operates on a list of DataMappers rather than a single instance. Otherwise, the converter code is going to get pretty messy. If that is acceptable, then the following converter should give you what you want:

public class DataMapperListConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<DataMapper>);
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
List<DataMapper> list = (List<DataMapper>)value;
if (list.Any(dm => dm.DataMapperProperty != null))
{
JObject obj = new JObject(list.Select(dm =>
{
JToken val;
if (dm.DataMapperProperty != null)
val = JToken.FromObject(dm.DataMapperProperty, serializer);
else
val = JToken.FromObject(dm.SubDataMappers, serializer);
return new JProperty(dm.Name, val);
}));
obj.WriteTo(writer);
}
else
{
serializer.Serialize(writer,
list.Select(dm => new Dictionary<string, List<DataMapper>>
{
{ dm.Name, dm.SubDataMappers }
}));
}
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Object)
{
return token.Children<JProperty>()
.Select(jp =>
{
DataMapper mapper = new DataMapper { Name = jp.Name };
JToken val = jp.Value;
if (val["data-type"] != null)
mapper.DataMapperProperty = jp.Value.ToObject<DataMapperProperty>(serializer);
else
mapper.SubDataMappers = jp.Value.ToObject<List<DataMapper>>(serializer);
return mapper;
})
.ToList();
}
else if (token.Type == JTokenType.Array)
{
return token.Children<JObject>()
.SelectMany(jo => jo.Properties())
.Select(jp => new DataMapper
{
Name = jp.Name,
SubDataMappers = jp.Value.ToObject<List<DataMapper>>(serializer)
})
.ToList();
}
else
{
throw new JsonException("Unexpected token type: " + token.Type.ToString());
}
}
}

Assumptions:

  • You will never be serializing a single DataMapper by itself; it will always be contained in a list.
  • DataMappers can be nested to an arbitrary depth.
  • A DataMapper will always have a non-null Name, which is unique at each level.
  • A DataMapper will never have both a non-null DataMapperProperty and a non-empty list of SubDataMappers.
  • A DataMapperProperty will always have a non-null DataType.
  • A DataMapper will never have a Name of data-type.

If the last four assumptions do not hold true, then this JSON format will not work for what you are trying to do, and you will need to rethink.

To use the converter, you will need to add it to your serializer settings as shown below. Use the settings both when you serialize and deserialize. Remove the [JsonConverter] attribute from the DataMapper class.

var settings = new JsonSerializerSettings()
{
Converters = new List<JsonConverter> { new DataMapperListConverter() },
Formatting = Formatting.Indented
};

Here is a round-trip demo: https://dotnetfiddle.net/8KycXB

How to move JSON object's depth one level up in JSON.net using C#?

I was able to get the desired output by moving the converter to the class instead of the property and ignoring the property using [JsonIgnore]. The property needs to be ignored since the converter for the class will generate the JSON for the property as shown below.

So the JsonObject class will be:

[JsonConverter(typeof(JsonObjectConverter))]
public class JsonObject
{
public JsonObject(IDataObject dataObject)
{
this.DataObject= dataObject;
}

[JsonIgnore]
public IDataObject DataObject;
}

Then create the converter like this:

public class JsonObjectConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(IDataObject).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JsonObject jsonObject = (JsonObject)value;
if (jsonObject.DataObject.GetType() == typeof(MyDataObject))
{
MyDataObject dataObject = (MyDataObject) jsonObject.DataObject;

writer.WriteStartObject();

writer.WritePropertyName("id");
writer.WriteValue(dataObject.Id);
writer.WritePropertyName("name");
writer.WriteValue(dataObject.Name);

writer.WriteEndObject();
}
}
}

This gives the desired output of:

{"id":"1","name":"data name"}

JSON nesting too deep when serializing in Web API

You have not provided the full model schema, but it seems that you don't need these settings in this case.

I suggest you to read accepted answer for this question which has a great explanation of how ReferenceLoopHandling and PreserveReferencesHandling settings are working. In general it's all about when you have the same object instance more than once in object tree that you want to serialize. Looking at your image it's not the case.

It seems that you are serializing your EF models and you are using lazy loading for connected objects. You might think of creating separate model for JSON serialization and populate only fields you need. Using this technique you will be sure what data is retrieved from DB.

Another option would be to use JsonIgnore attribute. You can mark needed properties in your EF models. For example:

public class A
{
public string NameOfA { get; set; }

[JsonIgnore]
public string IgnoredProperty { get; set; }

[JsonIgnore]
public B B { get; set; }
}

public class B
{
public string NameOfB { get; set; }
}

And if we execute this code:

var a = new A
{
NameOfA = "A",
IgnoredProperty = "Will be ignored",
B = new B
{
NameOfB = "B"
}
};
var json = JsonConvert.SerializeObject(a, Formatting.Indented);

The resulting JSON will be:

{
"NameOfA": "A"
}

JSON .NET Custom Name Resolver for Sub-Properties

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?

Order of serialized fields using JSON.NET

The supported way is to use the JsonProperty attribute on the class properties that you want to set the order for. Read the JsonPropertyAttribute order documentation for more information.

Pass the JsonProperty an Order value and the serializer will take care of the rest.

 [JsonProperty(Order = 1)]

This is very similar to the

 DataMember(Order = 1) 

of the System.Runtime.Serialization days.

Here is an important note from @kevin-babcock

... setting the order to 1 will only work if you set an order greater than 1 on all other properties. By default any property without an Order setting will be given an order of -1. So you must either give all serialized properties and order, or set your first item to -2



Related Topics



Leave a reply



Submit