4

I have to read a JSON document, which has a field that can contain different types. For example can be either a long or an array of integers. I know I will need to use a custom deserializer, but am not sure how. In the example below the xx field sometimes is a long, otherwise an array of ints. Any help on how to deal with this is appreciated.

        static void JsonTest() {
           const string json = @"
  {
     'Code': 'XYZ',
     'Response': {
        'Type' : 'S',
        'Docs': [
           { 
              'id' : 'test1',
              'xx' : 1
           },
           { 
              'id' : 'test2',
              'xx' : [1, 2, 4, 8]
           },
        ]
     }
  }";
           A a;
           try {
              a = JsonConvert.DeserializeObject<A>(json);
           }
           catch( Exception ex ) {
              Console.Error.WriteLine(ex.Message);
           }
        }

        public class A {
           public string Code;
           public TResponse Response;
        }

        public class TResponse {
           public string Type;
           public List<Doc> Docs;
        }

        public class Doc {
           public string id;
           public int[] xx;
        }

My implementation based on the suggestion below (changed array to long from int):

  [JsonConverter(typeof(DocConverter))]
  public class Doc {
     public string id;
     public long[] xx;
  }

  public class DocConverter : JsonConverter {
     public override bool CanWrite { get { return false; } }

     public override bool CanConvert( Type objectType ) {
        return typeof(Doc).IsAssignableFrom(objectType);
     }

     public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) {
        JObject item = JObject.Load(reader);
        Doc doc = new Doc();
        doc.id = item["id"].ToObject<string>();
        if( item["xx"].Type == JTokenType.Long )
           doc.xx = new [] { item["xx"].ToObject<long>() };
        else
           doc.xx = item["xx"].ToObject<long[]>();
        return doc;
     }

     public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer ) {
        throw new NotImplementedException();
     }
  }
GOancea
  • 73
  • 1
  • 4

2 Answers2

2

Since xx can either be a long or an array of ints, it makes sense to turn Doc into a class hierarchy. (If it were a single long or an array of longs, it would make sense to read them all into a single class.)

You can do this by using a JsonConverter, like so:

[JsonConverter(typeof(DocConverter))]
public abstract class Doc
{
    public string id;
}

[JsonConverter(typeof(NoConverter))] // Prevents infinite recursion when converting a class instance known to be of type DocSingle
public class DocSingle : Doc
{
    public long xx;
}

[JsonConverter(typeof(NoConverter))] // Prevents infinite recursion when converting a class instance known to be of type DocList
public class DocList : Doc
{
    public int[] xx;
}

public class DocConverter : JsonConverter
{
    public override bool CanWrite { get { return false; } }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Doc).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, 
        Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject item = JObject.Load(reader);
        if (item["xx"].Type == JTokenType.Integer)
        {
            return item.ToObject<DocSingle>();
        }
        else
        {
            return item.ToObject<DocList>();
        }
    }

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

public class NoConverter : JsonConverter
{
    public override bool CanRead { get { return false; } }

    public override bool CanWrite { get { return false; } }

    public override bool CanConvert(Type objectType)
    {
        return false;
    }

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

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

Update

incidentally, if you're willing to simplify your data model to say that xx can either be a single long or an array of longs, you can simplify the code as follows:

[JsonConverter(typeof(DocConverter))]
public sealed class Doc
{
    public string id;
    public long[] xx;
}

public class DocConverter : JsonConverter
{
    public override bool CanWrite { get { return true; } }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Doc).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader,
        Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject item = JObject.Load(reader);
        var doc = new Doc();

        JToken id = item["id"];
        if (id != null)
            doc.id = id.ToString();
        JToken xx = item["xx"];
        if (xx != null)
        {
            if (xx.Type == JTokenType.Integer)
            {
                var val = (long)xx;
                doc.xx = new long[] { val };
            }
            else if (xx.Type == JTokenType.Array)
            {
                var val = xx.ToObject<long[]>();
                doc.xx = val;
            }
            else
            {
                Debug.WriteLine("Unknown type of JToken for \"xx\": " + xx.ToString());
            }
        }

        return doc;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var doc = (Doc)value;
        writer.WriteStartObject();
        writer.WritePropertyName("id");
        writer.WriteValue(doc.id);
        var xx = doc.xx;
        if (xx != null)
        {
            writer.WritePropertyName("xx");
            if (xx.Length == 1)
            {
                writer.WriteValue(xx[0]);
            }
            else
            {
                writer.WriteStartArray();
                foreach (var x in xx)
                {
                    writer.WriteValue(x);
                }
                writer.WriteEndArray();
            }
        }
        writer.WriteEndObject();
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
  • @GOancea - added a simplified version. – dbc Nov 25 '14 at 17:25
  • 1
    I ended up using something based on what you suggested, but avoided using the derived classes. Just used the Doc and set the class fields inside the ReadJson method based on the item["xx"] and item["id"]. – GOancea Nov 25 '14 at 17:42
  • Pretty much what I did above. Thanks. Do not need the Write but it's good to know. – GOancea Nov 25 '14 at 18:07
0

You have a string, try json.Contains("'Type':'S'"). Then deserialize it to the proper model.

brduca
  • 3,573
  • 2
  • 22
  • 30