6

I am looking to properly describe column metadata in JSON which is later parsed by Newtonsoft to build a C# DataTable. In this way, I'm hoping to solve a problem of getting a DataTable with no rows or no columns, but where I need the columns to be created with a label and hopefully with a data type, even when I pass an empty table.

An example for standard input:

{
    "BrokerID": "998",
    "AccountID": "1313",
    "Packages": [
        {
            "PackageID": 226,
            "Amount": 15000,
            "Auto_sync": true,
            "Color": "BLUE"
        },
        {
            "PackageID": 500,
            "Amount": 15000,
            "Auto_sync": true,
            "Color": "PEACH"
        }
    ]
}

An example of input with an empty table:

{
    "BrokerID" : "998",
    "AccountID" : "1313",
    "Packages":[]
}

When I parse this using JsonConvert.DeserializeObject<DataTable>(params["Packages"]);, I get no rows and, obviously, no columns. I'm looking for a way to describe the columns metadata in the JSON's body.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Ben Guri
  • 306
  • 3
  • 12

2 Answers2

11

The DataTableConverter that ships with Json.Net does not output column metadata, even if you set TypeNameHandling to All. However, there's nothing to prevent you from making your own custom converter that does this, and using that instead. Here's one I threw together that might suit your needs:

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

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        DataTable dt = (DataTable)value;
        JObject metaDataObj = new JObject();
        foreach (DataColumn col in dt.Columns)
        {
            metaDataObj.Add(col.ColumnName, col.DataType.AssemblyQualifiedName);
        }
        JArray rowsArray = new JArray();
        rowsArray.Add(metaDataObj);
        foreach (DataRow row in dt.Rows)
        {
            JObject rowDataObj = new JObject();
            foreach (DataColumn col in dt.Columns)
            {
                rowDataObj.Add(col.ColumnName, JToken.FromObject(row[col]));
            }
            rowsArray.Add(rowDataObj);
        }
        rowsArray.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JArray rowsArray = JArray.Load(reader);
        JObject metaDataObj = (JObject)rowsArray.First();
        DataTable dt = new DataTable();
        foreach (JProperty prop in metaDataObj.Properties())
        {
            dt.Columns.Add(prop.Name, Type.GetType((string)prop.Value, throwOnError: true));
        }
        foreach (JObject rowDataObj in rowsArray.Skip(1))
        {
            DataRow row = dt.NewRow();
            foreach (DataColumn col in dt.Columns)
            {
                if (rowDataObj[col.ColumnName].Type != JTokenType.Null)//Skip if the Value is Null/Missing, especially for a non-nullable type.
                    row[col] = rowDataObj[col.ColumnName].ToObject(col.DataType);
            }
            dt.Rows.Add(row);
        }
        return dt;
    }
}

Here is a demo. Notice that the column types are written out as the first row of the array in the JSON when the table is serialized. On deserialization, this metadata is used to reconstruct the table with the correct column types and names, even if there are no other rows. (You can verify this by commenting out the two lines at the top where the row data is added to the table.)

class Program
{
    static void Main(string[] args)
    {
        DataTable dt = new DataTable();
        dt.Columns.Add("PackageID", typeof(int));
        dt.Columns.Add("Amount", typeof(int));
        dt.Columns.Add("Auto_sync", typeof(bool));
        dt.Columns.Add("Color", typeof(string));
        // Comment out these two lines to see the table with no data.
        // Test with a null Value for a Non-Nullable DataType.
        dt.Rows.Add(new object[] { 226,  null, true, "BLUE" });
        dt.Rows.Add(new object[] { 500, 15000, true, "PEACH" });

        Foo foo = new Foo
        {
            BrokerID = "998",
            AccountID = "1313",
            Packages = dt
        };

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.Converters.Add(new CustomDataTableConverter());
        settings.Formatting = Formatting.Indented;

        string json = JsonConvert.SerializeObject(foo, settings);
        Console.WriteLine(json);
        Console.WriteLine();

        Foo foo2 = JsonConvert.DeserializeObject<Foo>(json, settings);
        Console.WriteLine("BrokerID: " + foo2.BrokerID);
        Console.WriteLine("AccountID: " + foo2.AccountID);
        Console.WriteLine("Packages table:");
        Console.WriteLine("  " + string.Join(", ", 
            foo2.Packages.Columns
                .Cast<DataColumn>()
                .Select(c => c.ColumnName + " (" + c.DataType.Name + ")")));

        foreach (DataRow row in foo2.Packages.Rows)
        {
            Console.WriteLine("  " + string.Join(", ", row.ItemArray
                .Select(v => v != null ? v.ToString() : "(null)")));
        }
    }
}

class Foo
{
    public string BrokerID { get; set; }
    public string AccountID { get; set; }
    public DataTable Packages { get; set; }
}

Output:

{
  "BrokerID": "998",
  "AccountID": "1313",
  "Packages": [
    {
      "PackageID": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
      "Amount": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
      "Auto_sync": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
      "Color": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    },
    {
      "PackageID": 226,
      "Amount": null,
      "Auto_sync": true,
      "Color": "BLUE"
    },
    {
      "PackageID": 500,
      "Amount": 15000,
      "Auto_sync": true,
      "Color": "PEACH"
    }
  ]
}

BrokerID: 998
AccountID: 1313
Packages table:
  PackageID (Int32), Amount (Int32), Auto_sync (Boolean), Color (String)
  226, , True, BLUE
  500, 15000, True, PEACH

Fiddle: https://dotnetfiddle.net/GGrn9z

MikeTeeVee
  • 18,543
  • 7
  • 76
  • 70
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
0

You can add a schema to your json: http://json-schema.org/example1.html

What does your api method signature look like? If you are passing in a json string to be deserialized into an object you would already have the column info on the object itself. I would suggest changing your api method signature to something like:

[HttpPost, Route("packages")]
public IHttpActionResult Packages(IEnumerable<Package> packages)

You could then use that list for your data tables or just enumerate the list as it is.

Stephen Brickner
  • 2,584
  • 1
  • 11
  • 19
  • I need the input json and the object to be loosely coupled to reduce maintenance in the future, this is why i need to be able to read the columns meta data from the user and to be able to cope with an empty table aswell. the link you provided looks like a step in that direction but it doesn't provide a useful example of a JSON object that is sent with its own describing metadata. – Ben Guri Nov 03 '15 at 14:02
  • You cannot send an empty json object so your endpoint would never be hit. The best you could do in this case if you are trying to show an empty row with the column headers you want to display would be send your list of data in with the first one in the list as the schema with the properties set to some default so they will never be empty and come across as a full set of columns. In the event there are no rows you would always have at least the first one. It's a hack but WebAPI isn't intended to be used in this way. – Stephen Brickner Nov 03 '15 at 14:28
  • Json Schema is useful for defining ... Json schemas and Json.Net supports them. APIs on the other hand tend to use Swagger (available in Web API through addins like SwashBuckle). Which one you choose depends on usage – Panagiotis Kanavos Nov 03 '15 at 14:35
  • an empty json is never sent, the field that describes a table (list of json objects) might be given with an empty list, in this case i will have to be provided with some the columns metadata in order to build an empty datatable of the "right structure". anyway it looks ill just default to a hardcoded type in the server side. – Ben Guri Nov 03 '15 at 14:35