1

I'm trying to read a json fragment in a C# application.

In the fragment, there's an array that can contains either a simple string or a complex object. This is because the contained object has only one mandatory string, other fields are optional. Thus, to simplify the json, the array can contain a string (= default parameters) or the complex object.

Here is a sample json :

{
    "Config1": [
        "Simple string 1",
        "Simple string 2",
        {
            "Data": "Complex object",
            "OptionalField": "some option",
            "AnotherOption": 42
        }
    ],
    "Config3": [
        "Simple string 3",
        "Simple string 4",
        {
            "Data": "Complex object 2",
            "OptionalField": "some option",
            "AnotherOption": 12
        }
    ]    
}

The corresponding C# model :

public class Config : Dictionary<string, ConfigItem[]>
{
}
public class ConfigItem
{
    public ConfigItem()
    {
    }

    public ConfigItem(string data)
    {
        this.Data = data;
    }

    public string Data { get; set; }

    public string OptionalField { get; set; }

    public int AnotherOption { get; set; }
}

In my sample, only the Data field is mandatory. When a string is supplied, the constructor with a single string parameter must be called. When a complex json object is provided, the standard deserialization must run.

For example, these two json fragments are equivalent (within the array):

"Config4": [
    "This is a string",
    {
        "Data": "This is a string",
        "OptionalField": null,
        "AnotherOption": 0
    }
]

How to reach my goal ?

Right now, I tried to implement a custom converter :

[JsonConverter(typeof(StringToComplexConverter))]
public class ConfigItem
{
    // Properties omiited
}

public class StringToComplexConverter : JsonConverter<ConfigItem>
{
    public override bool CanWrite => false;

    public override ConfigItem ReadJson(JsonReader reader, Type objectType, ConfigItem existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if(reader.TokenType == JsonToken.String)
        {
            var ctor = objectType.GetConstructor(new[] { typeof(string) });
            return (ConfigItem)ctor.Invoke(new[] { (string)reader.Value });
        }
        else
        {
            // What to put in the else ?
        }
    }

    public override void WriteJson(JsonWriter writer, ConfigItem value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

This is actually working for strings. However, I didn't find what to put in the else statement. Is there a way to forward to standard deserialization from the ReadJson method ?

If I put return serializer.Deserialize<ConfigItem>(reader); in my else statement, it ends in a infinite loop.

Also tried return serializer.Deserialize<JObject>(reader).ToObject<ConfigItem>();, with no success.

Steve B
  • 36,818
  • 21
  • 101
  • 174
  • I think you need to inherit from the non-generic `JsonConverter`. –  Nov 29 '18 at 14:29
  • @Amy: not sure to get your point. Using the non-generic converter, I still got an infinite loop. – Steve B Nov 29 '18 at 14:35
  • Implementing the generic converter means you can only return objects of that type. You can't call `serializer.Deserialize...` because it just uses your converter, but I'm reasonably certain you need to implement the non-generic converter. –  Nov 29 '18 at 14:36
  • See [this question](https://stackoverflow.com/questions/20432166/how-to-deserialize-a-json-property-that-can-be-two-different-data-types-using-js). Does that help you deserialize the object? –  Nov 29 '18 at 14:43
  • See also [this question](https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n). This one might be a dupe. I'm not sure. I still haven't had any coffee this morning :( –  Nov 29 '18 at 14:44
  • @Amy: both questions have answers that add the converter attribute to the property of a wrapper object. In my case, I put the converter directly on the ConfigItem class. Because of that, my converter is always called, whether I'm calling `serializer.Deserialise` in my readjson override or if I load a `JObject` and then call `.ToObject` onto it – Steve B Nov 29 '18 at 14:48

1 Answers1

1

Finally found a way. Here's my else statement:

        else
        {
            var result = new ConfigItem();
            serializer.Populate(reader, result);
            return result;
        }

Bonus: here's a generic variant of the converter that can be applied to any class that have a constructor with no parameter and another with a string parameter:

public class StringToComplexConverter<TObject> : JsonConverter<TObject>
{
    public override bool CanWrite => false;

    public override TObject ReadJson(
        JsonReader reader,
        Type objectType,
        TObject existingValue,
        bool hasExistingValue,
        JsonSerializer serializer
        )
    {
        if (reader.TokenType == JsonToken.String)
        {
            var ctor = objectType.GetConstructor(new[] { typeof(string) });
            return (TObject)ctor.Invoke(new[] { (string)reader.Value });
        }
        else
        {
            var result = (TObject)Activator.CreateInstance(objectType);
            serializer.Populate(reader, result);
            return result;
        }
    }

    public override void WriteJson(JsonWriter writer, TObject value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
Steve B
  • 36,818
  • 21
  • 101
  • 174