3

I'd like to have property of type object that can be either a string or Template type.

Is it possible to tell Json.NET to parse something into one of several specified types?

class MyClass
{
    public object Template { get; set; }
}

where Template = "TemplateName"

{
    "Template": "TemplateName"
}

or Template = new Template()

{
    "Template": { "Name": "OtherTamplate", ... }
}

UPDATE:

I tried to follow @krillgar' advice and create a custom JsonConverter but unfortunatelly the CanConvert method receives only the target type, in this case object. This information is not enough to tell wheter it can be deserialized (in case I had other object properties). I guess I need it to be a Template after all or create a derived type like TemplateReference or something:

class myconverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type objectType)
    {
        // objectType = typeof(object)
        throw new NotImplementedException();
    }
}

Configuration = JsonConvert.DeserializeObject<MyClass>(text, new myconverter());

Disclaimer

This question has once been closed as a duplicate of How to deserialize a JSON property that can be two different data types using Json.NET. Because at the time of writing my question I hadn't known that there already was a similar one I'd like to clarify the difference between them to prevent it from being closed in future:

The other question is about how to deserialize different values into a concrete type whereas mine is about deserializing different values into an object. It might seem to be the same at the first look because in both examples only the type of the property is different but it has a huge impact on the overall application design. It's important for me that I can use an object to store different specialized types rather then one type having multiple responsibilities.

Community
  • 1
  • 1
t3chb0t
  • 16,340
  • 13
  • 78
  • 118
  • Of course. You'll have to create a [custom deserializer](http://geekswithblogs.net/DavidHoerster/archive/2011/07/26/json.net-custom-convertersndasha-quick-tour.aspx) for it. – krillgar Oct 12 '15 at 12:18
  • @krillgar: thank you for the clue :) I hadn' thought I would need it to be that _complicated_. I had hoped there would be some _secret_ attribute to add the to property and specify possible types or a setting for the deserializer. – t3chb0t Oct 12 '15 at 12:24
  • You need a custom desiralizer for MyClass, not for Template. The MyClass desiralizer will then have to decide whether the template property is a string or Template, and call the appropriate standard deserializer. – Ben Oct 12 '15 at 13:01
  • Great, thanks for closing this as a duplicate. The linked question doesn't answer my question in any way. I want to deserialize multiple types into object and not as a specific type. – t3chb0t Oct 12 '15 at 14:13
  • @Ben I like the idea however I'm not sure if I really need to parse each property myself. If I create a converter for `MyClass` and override the `ReadJson` method can I just handle the type or property that I'd like to handle differently and use some default converter for everything else? – t3chb0t Oct 12 '15 at 14:42

3 Answers3

2

This problem can be solved by using a custom JsonConverter. Here is a generic version that should work for this situation:

class ObjectOrStringConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        // CanConvert is not called when the [JsonConverter] attribute is used
        return false;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Object)
        {
            return token.ToObject<T>(serializer);
        }
        return token.ToString();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

To use the converter, all you need to do is add a [JsonConverter] attribute to the property in your class that can be either a string or an object. The generic type parameter must match the type of non-string object you are expecting.

class MyClass
{
    [JsonConverter(typeof(ObjectOrStringConverter<Template>))]
    public object Template { get; set; }
}

Below is a demonstration of the converter in action:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("--- first run ---");

        string json = @"
        {
            ""Template"": ""TemplateName""
        }";

        DeserializeAndDump(json);

        Console.WriteLine("--- second run ---");

        json = @"
        {
            ""Template"": { ""Name"": ""OtherTemplate"" }
        }";

        DeserializeAndDump(json);
    }

    static void DeserializeAndDump(string json)
    {
        MyClass obj = JsonConvert.DeserializeObject<MyClass>(json);
        if (obj.Template == null)
        {
            Console.WriteLine("Template property is null");
        }
        else
        {
            Console.WriteLine("Template property is a " + obj.Template.GetType().Name);

            string name = "(unknown)";
            if (obj.Template is Template) name = ((Template)obj.Template).Name;
            else if (obj.Template is string) name = (string)obj.Template;

            Console.WriteLine("Template name is \"" + name + "\"");
        }
        Console.WriteLine();
    }
}

class Template
{
    public string Name { get; set; }
}

And here is the output from the above:

--- first run ---
Template property is a String
Template name is "TemplateName"

--- second run ---
Template property is a Template
Template name is "OtherTemplate"

Fiddle: https://dotnetfiddle.net/Lw3RaN

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • Thank you :) I now see what I was doing wrong. During my experiments I had both the attribute specified and the converter passed to the `DeserializeObject` method. I didn't know I can only use the attribute so that the `CanConvert` method won't be called. This makes the whole task much simpler :] I'll check it later. – t3chb0t Oct 13 '15 at 08:24
  • Ah. Yes, that can be a little bit of a "gotcha" if you haven't done a converter before. – Brian Rogers Oct 13 '15 at 13:54
1

I don't know if you can do that, but you could go another way. Change your "Template" property to be a Template instead of an object and use a custom property of the Template class to know weither you want to serialize it as a Template or a string.

class MyClass
{
    [JsonConverter(typeof(TemplateConverter))]
    public Template Template { get; set; }
}

class Template
{
    /* Your Template class */

    public Type TypeToSerializeInto { get; private set; }
}

public class TemplateConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) 
    { 
        return objectType == typeof(Template);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
    { 
        Template val = value as Template;

        writer.WriteStartObject();
        writer.WritePropertyName("Template");

        if (val.TypeToSerializeInto == typeof(Template))
            serializer.Serialize(writer, val);
        else if (val.TypeToSerializeInto == typeof(string))
            serializer.Serialize(writer, val.Name);

        writer.WriteEndObject();
    }
}
Arthur Rey
  • 2,990
  • 3
  • 19
  • 42
0

If you have a property, which type is an abstract type - like object, on de-serialization, you can know the specific type that was serialized by serializing somewhere also the name of that specific type. So your json should look like this:

{ "MyClass": { "Template": "some name", "type": "System.String" } }

This way on deserialization you can check what type was that property before the serialization (in this case String)

Another way to determine the type is by checking the json structure as you can see here: C#: Deserializing JSON when one field can be different types

Community
  • 1
  • 1
Boies Ioan
  • 892
  • 8
  • 12
  • I don't think it's a good idea to put the actual type there but thank you for the suggestion anyway :) – t3chb0t Oct 12 '15 at 16:05