For example we have two classes
class FooA
{
[SomeSpecialAttribute]
public int SomeValueA { get; set; }
public int SomeValueB { get; set; }
public int SomeValueC { get; set; }
}
class FooB
{
public FooA FooA { get; set; }
}
I use Json.NET, max depth is 1. While serializing FooA it should output all properties as usual, but while serializing FooB it should output only one FooA's property which has special attribute. So only while resolving nested reference properties (Depth > 0) we should get a single field.
Output should be: { "FooA": { "SomeValueA": "0" } }
Any ideas?
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()
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With