Your idea of using an inner serializer will not work as-is. The id-to-object mapping table is held in a private field JsonSerializerInternalBase._mappings
with no way to copy it from the outer serializer to the inner serializer.
As an alternative, you could make a recursive call to serialize using the same serializer, and have the converter disable itself using a thread-static pushdown stack along the lines of Generic method of modifying JSON before being returned to client and JSON.Net throws StackOverflowException when using [JsonConvert()]. You would need to enhance the converters in these examples to manually check for and add the necessary "$id"
and "$ref"
properties by making use of the JsonSerializer.ReferenceResolver
property.
However, since your converters just add properties, a more straightforward solution to your problem might be to create a custom contract resolver that allows types to customize their contract as it is generated via a callback method declared in an attribute applied to the type, for instance:
public class ModifierContractResolver : DefaultContractResolver
{
// 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."
// See also https://stackoverflow.com/questions/33557737/does-json-net-cache-types-serialization-information
static ModifierContractResolver instance;
// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
static ModifierContractResolver() { instance = new ModifierContractResolver(); }
public static ModifierContractResolver Instance { get { return instance; } }
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
// Apply in reverse order so inherited types are applied after base types.
foreach (var attr in objectType.GetCustomAttributes<JsonObjectContractModifierAttribute>(true).Reverse())
{
var modifier = (JsonObjectContractModifier)Activator.CreateInstance(attr.ContractModifierType, true);
modifier.ModifyContract(objectType, contract);
}
return contract;
}
}
public abstract class JsonObjectContractModifier
{
public abstract void ModifyContract(Type objectType, JsonObjectContract contract);
}
[System.AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class JsonObjectContractModifierAttribute : System.Attribute
{
private readonly Type _contractModifierType;
public Type ContractModifierType { get { return _contractModifierType; } }
public JsonObjectContractModifierAttribute(Type contractModifierType)
{
if (contractModifierType == null)
{
throw new ArgumentNullException("contractModifierType");
}
if (!typeof(JsonObjectContractModifier).IsAssignableFrom(contractModifierType))
{
throw new ArgumentNullException(string.Format("{0} is not a subtype of {1}", contractModifierType, typeof(JsonObjectContractModifier)));
}
this._contractModifierType = contractModifierType;
}
}
Then, apply it to your types as in the following example:
[JsonObjectContractModifier(typeof(TestContractModifier))]
public class Test
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
}
class TestContractModifier : JsonObjectContractModifier
{
class EmptyValueProvider : IValueProvider
{
// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
static EmptyValueProvider() { }
internal static readonly EmptyValueProvider Instance = new EmptyValueProvider();
#region IValueProvider Members
public object GetValue(object target)
{
var test = target as Test;
if (test == null)
return null;
return test.A == null && test.B == null && test.C == null;
}
public void SetValue(object target, object value)
{
var property = target as Test;
if (property == null)
return;
if (value != null && value.GetType() == typeof(bool) && (bool)value == true)
{
property.A = property.B = property.C = null;
}
}
#endregion
}
public override void ModifyContract(Type objectType, JsonObjectContract contract)
{
var jsonProperty = new JsonProperty
{
PropertyName = "isEmpty",
UnderlyingName = "isEmpty",
PropertyType = typeof(bool?),
NullValueHandling = NullValueHandling.Ignore,
Readable = true,
Writable = true,
DeclaringType = typeof(Test),
ValueProvider = EmptyValueProvider.Instance,
};
contract.Properties.Add(jsonProperty);
}
}
And serialize as follows:
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects, // Or PreserveReferencesHandling.All
ContractResolver = ModifierContractResolver.Instance,
};
var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
This produces the following JSON:
[
{
"$id": "1",
"A": "hello",
"B": "goodbye",
"C": "sea",
"isEmpty": false
},
{
"$ref": "1"
},
{
"$id": "2",
"A": null,
"B": null,
"C": null,
"isEmpty": true
},
}
As you can see, both the synthetic "isEmpty"
property and reference handling properties are present. Prototype fiddle.