1

I have a family of custom Json Converters. They work like this:

public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) {
  JObject jo = JObject.FromObject(value);
  // do my own stuff to the JObject here -- basically adding a property. The value of the property depends on the specific converter being used.
  jo.WriteTo(writer, this);
}

The problem with this is that the id field of the JObject is always 1. Not good. So I tried using an inner serializer to get the id field:

private JsonSerializer _InnerSerializer {get;set;}
private JsonSerializer InnerSerializer {
  get {
    if (_InnerSerializer == null) {
      _InnerSerializer = new JsonSerializer();
    }
    return _InnerSerializer;
  }
}

public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) {
  JsonSerializer inner = this.InnerSerializer;
  jo = JObject.FromObject(value, inner);
  //my stuff here
  jo.WriteTo(writer, this);
}

That gives a different id each time, even if it hits the same object twice. What I really want is to use Json's usual id resolution with my custom serialization. How can I do that?

William Jockusch
  • 26,513
  • 49
  • 182
  • 323
  • Incidentally, one thing that would solve my problem would be if there were a way to pass the converter as an argument when getting the value to serialize for the property. – William Jockusch Jun 11 '16 at 09:04
  • This isn't so easy. There's no equivalent to [`PopulateObject()`](https://stackoverflow.com/questions/35586987) for populating a pre-allocated `JObject`. For some workarounds see [Generic method of modifying JSON before being returned to client](https://stackoverflow.com/questions/35532466) and [JSON.Net throws StackOverflowException when using JsonConvert()](https://stackoverflow.com/questions/29719509). – dbc Jun 11 '16 at 12:54
  • 1
    Can you give more details about your custom converter? If all you are doing is adding a property, it might be easier to do it with a [custom contract resolver](http://www.newtonsoft.com/json/help/html/ContractResolver.htm). – dbc Jun 11 '16 at 12:57
  • I really am just adding a property, with a value that is currently computed by my custom converter (the value depends on both the object being serialized and which converter is being used.) All it has to do is come up with a custom value based on both the object being serialized and custom settings for that particular call to JsonConvert.SerializeObject(whatever). Investigating the contract resolver thing right now. That does look more promising. – William Jockusch Jun 11 '16 at 13:29

1 Answers1

1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340