0

I want to replicate the TypeNameHandling = TypeNameHandling.Objects setting but have my own property name, rather than $type, and have it find the objects based on the simple class name rather than have the assembly referenced.

I have a nested object model that I am trying to sterilise using the Newtonsoft tool. When I run it I get a System.StackOverflowException and I really cant figure out why... I have reviewed Custom JsonConverter WriteJson Does Not Alter Serialization of Sub-properties and the solution there does not work natively within Newtonsoft and thus ignores all of the Newtonsoft native attributes.

If I pass a single convertor (all the object inherit from IOptions) I get only the top-level object with the required ObjectType:

{
  "ObjectType": "ProcessorOptionsA",
  "ReplayRevisions": true,
  "PrefixProjectToNodes": false,
  "CollapseRevisions": false,
  "WorkItemCreateRetryLimit": 5,
  "Enabled": true,
  "Endpoints": null,
  "ProcessorEnrichers": [
    {
      "Enabled": true
    },
    {
      "Enabled": true
    }
  ]
}

I have 4 classes that all have my custom OptionsJsonConvertor set as the convertor.

[JsonConverter(typeof(OptionsJsonConvertor<IProcessorEnricherOptions>))]
public interface IProcessorEnricherOptions : IEnricherOptions
{
}

[JsonConverter(typeof(OptionsJsonConvertor<IProcessorOptions>))]
public interface IProcessorOptions : IProcessorConfig, IOptions
{
    List<IEndpointOptions> Endpoints { get; set; }

    List<IProcessorEnricherOptions> ProcessorEnrichers { get; set; }

    IProcessorOptions GetDefault();
}

[JsonConverter(typeof(OptionsJsonConvertor<IEndpointOptions>))]
public interface IEndpointOptions : IOptions
{
    [JsonConverter(typeof(StringEnumConverter))]
    public EndpointDirection Direction { get; set; }

    public List<IEndpointEnricherOptions> EndpointEnrichers { get; set; }
}

[JsonConverter(typeof(OptionsJsonConvertor<IEndpointEnricherOptions>))]
public interface IEndpointEnricherOptions : IEnricherOptions
{
}

The object model does not nest the same object type at any point, but does have List<IEndpointEnricherOptions> contained within List<IEndpointOptions> contained within List<IProcessorOptions>.

  "Processors": [
    {
      "ObjectType": "ProcessorOptionsA",
      "Enabled": true,
      "ProcessorEnrichers": [
        {
          "ObjectType": "ProcessorEnricherOptionsA",
          "Enabled": true
        },
        {
          "ObjectType": "ProcessorEnricherOptionsB",
          "Enabled": true,
        }
      ],
      "Endpoints": [
        {
          "ObjectType": "EndpointOptionsA",
          "EndpointEnrichers": [
            {
              "ObjectType": "EndpointEnricherOptionsA",
              "Enabled": true,
            }
          ]
        },
        {
          "ObjectType": "EndpointOptionsA",
          "EndpointEnrichers": [
            {
              "ObjectType": "EndpointEnricherOptionsA",
              "Enabled": true,

            },
            {
              "ObjectType": "EndpointEnricherOptionsB",
              "Enabled": true,
            }
          ]
        }
      ]
    }
  ]

I want to replicate the TypeNameHandling = TypeNameHandling.Objects setting but have my own property name as well as finding the objects, but everything else should be the same.

Right now I have public class OptionsJsonConvertor<TOptions> : JsonConverter which works for a single nested list, but no sub-lists.

public override void WriteJson(JsonWriter writer,object value,JsonSerializer serializer)
{
    JToken jt = JToken.FromObject(value);
    if (jt.Type != JTokenType.Object)
    {
        jt.WriteTo(writer);
    }
    else
    {
        JObject o = (JObject)jt;
        o.AddFirst(new JProperty("ObjectType", value.GetType().Name));
        o.WriteTo(writer);
    }
}

If I remove all of the [JsonConverter] class attributes then it executes and adds ObjectType to the IProcessorOptions, but not to any of the subtypes. However, with those attributes, I get System.StackOverflowException on JToken jt = JToken.FromObject(value);

I had thought that this was due to it being the same object type, however even with 4 custom JsonConverter classes that don't share a common codebase I get the same exception.

I'm stumped and really don't want to have the ugly "$type" = "MyAssmbly.Namespace.Class, Assembly" node!

UPDATE Even if I only have the OptionsJsonConvertor<IProcessorOptions> enabled on the Class IProcessorOptions I get a System.StackOverflowException.

  • This basically looks to be the same problem as the one from [JSON.Net throws StackOverflowException when using `[JsonConvert()]`](https://stackoverflow.com/q/29719509/3744182). Since your data structures are recursive, the "second, simpler workaround" from [this answer](https://stackoverflow.com/a/29720068/3744182) should work for you. (The first alternative from that answer will not work though.) – dbc Nov 16 '20 at 15:05

1 Answers1

0

OK, so the resolution for this was somewhat of a compromise. We now have no customer JsonConverter types in the system and instead, use ISerializationBinder.

public class OptionsSerializationBinder : ISerializationBinder
{
    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        typeName = serializedType.Name;
    }

    public Type BindToType(string assemblyName, string typeName)
    {
        Type type = AppDomain.CurrentDomain.GetAssemblies()
          .Where(a => !a.IsDynamic)
          .SelectMany(a => a.GetTypes())
          .FirstOrDefault(t => t.Name.Equals(typeName) || t.FullName.Equals(typeName));
        if (type is null || type.IsAbstract || type.IsInterface)
        {
            Log.Warning("Unable to load Processor: {typename}", typeName);
            throw new InvalidOperationException();
        }
        return type;
    }
}

Setting the Assembly option to null is critical to maintain the friendly names that we want.

    private static JsonSerializerSettings GetSerializerSettings(TypeNameHandling typeHandling = TypeNameHandling.Auto)
    {
        return new JsonSerializerSettings()
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            TypeNameHandling = typeHandling,
            TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
            SerializationBinder = new OptionsSerializationBinder(),
            Formatting = Formatting.Indented
        };
    }

We can then set SerializationBinder and the default the TypeNameHandling to auto. We found when setting this to Object that it was too greedy and tried to write a $type for generic lists and such, creating a nasty look that did not serilise. Auto provided the right level for us.

If you need to use Object which will also apply to the root of your object map then you may need to create a customer wrapper class around any List<> or Dictionary<> object that you want to serialise to make sure it gets a friendly name.

  • 1
    For performance reasons in `BindToType()` I'd suggest caching the value returned by the `AppDomain.CurrentDomain.GetAssemblies()` query in a `ConcurrentDictionary`. – dbc Nov 16 '20 at 15:05