Razor
Razor

Reputation: 17508

Deserialize JSON array of arrays

I have the following JSON data which I'd like to deserialize to a C# POCO object but I'm having trouble deserializing the array of arrays.

var json = @"{
  ""name"": ""Foo"",
  ""pages"": [
    {
         ""page"": 1,
          ""fields"": [
            {
                  ""name"": ""stuffs"",
                  ""rows"": [
                    [{ ""value"" : ""$199""}, { ""value"": ""foo"" }],
                    [{ ""value"" : ""$222""}, { ""value"": ""bar"", ""color"": ""blue"" }]
                  ]
        }]
    }
  ]
}";

The exception is

Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'UserQuery+TableRow' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
    To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.
    Path 'rows[0]', line 4, position 5.

Following the advise of the exception message, I did attempt all of those things but only to be faced with more errors.

These are my POCO objects

public class Document
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("pages")]
    public Page[] Pages { get; set; }
}

public class Page
{
    [JsonProperty("page")]
    public int PageNumber { get; set; }

    [JsonProperty("fields")]
    public FieldBase[] FieldsBase { get; set; }
}

public class TableRow
{
    public Cell[] Cells { get; set; }
}

public class Cell
{
    [JsonProperty("value")]
    public string Value { get; set; }

    [JsonProperty("color")]
    public string Color { get; set; }
}

public abstract class FieldBase
{
    [JsonProperty("name")]
    public string Name { get; set; }
}
public class Table : FieldBase
{
    [JsonProperty("rows")]
    public TableRow[] Rows { get; set; } = new TableRow[0];
}

And my field converter to deal with the abstract class (not sure if this matters)

public class FieldConverter : JsonConverter
{
    static JsonSerializerSettings SpecifiedSubclassConversion = new JsonSerializerSettings() { ContractResolver = new BaseSpecifiedConcreteClassConverter() };

    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(FieldBase));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        return JsonConvert.DeserializeObject<Table>(jo.ToString(), SpecifiedSubclassConversion);
    }

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

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException(); // won't be called because CanWrite returns false
    }
}

public class BaseSpecifiedConcreteClassConverter : DefaultContractResolver
{
    protected override JsonConverter ResolveContractConverter(Type objectType)
    {
        if (typeof(FieldBase).IsAssignableFrom(objectType) && !objectType.IsAbstract)
            return null; // pretend TableSortRuleConvert is not specified (thus avoiding a stack overflow)
        return base.ResolveContractConverter(objectType);
    }
}

And the following line of code which, when executed in LINQPad, produces the error

JsonConvert.DeserializeObject<Document>(json, new FieldConverter()).Dump();

Any help would be greatly appreciated.

Upvotes: 1

Views: 1505

Answers (1)

dbc
dbc

Reputation: 117155

In your json, "rows" is a jagged array:

"rows": [[{ "value" : "$199"}, { "value": "foo" }]]

However, in your object model this corresponds to an array of TableRow classes that contain an array of cells. Thus you will need another JsonConverter to serialize each TableRow as an array of cells and not an object:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var cells = serializer.Deserialize<Cell[]>(reader);
        return new TableRow { Cells = cells };
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var row = (TableRow)value;
        serializer.Serialize(writer, row.Cells);
    }
}

public class JsonDerivedTypeConverter<TBase, TDerived> : JsonConverter where TDerived : TBase
{
    public JsonDerivedTypeConverter()
    {
        if (typeof(TBase) == typeof(TDerived))
            throw new InvalidOperationException("TBase and TDerived cannot be identical");
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(TBase);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return serializer.Deserialize<TDerived>(reader);
    }

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

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

Then, to deserialize, do:

var settings = new JsonSerializerSettings
{
    Converters = new JsonConverter[] { new TableRowConverter(), new JsonDerivedTypeConverter<FieldBase, Table>() },
};

var doc = JsonConvert.DeserializeObject<Document>(json, settings);

Example fiddle.

Upvotes: 1

Related Questions