2

I'm attempting to serialize a .NET DataTable to a JSON file and then deserialize the JSON file back into a DataTable. Fairly straightforward I thought.

However, I have a table, 3 rows by 3 columns, each element is of type double. If any value in the first row is null, when JSON.Net deserializes the json file to a DataTable object, all values of the column that was null in the first row become strings.

To be clear, it is only if a value in the first row is null that this happens. If any values are null in any other row than the first, the remaining values in that column remain doubles.

  1. If I replace the null with a double, everything works as expected (I can't do this in my case, however).

  2. If I set NullValueHandling = NullValueHandling.Ignore, all values stay as doubles, except the first row now gets listed as the last row:

Example:

"Column2": 1.0,
"Column3": 1.1
},
{
   "Column1": 0.0,
   "Column2": 0.5,
   "Column3": 2.0
},

Becomes:

  "Column2": 1.0,
  "Column3": 1.1
},
{
  "Column2": 0.5,
  "Column3": 2.0,
  "Column1": 0.0
},

I need to be able to deserialize the JSON, keep the Columns in order, and not have null values in the first row cause all values in that row to become strings. I also need to keep the Column1 of the first row (in the case above) null - don't care if it is a null string or a DBNull.

Any thoughts? (My test code below..comment/uncomment NullValueHandling to see the problem)

        DataTable table = new DataTable("MyTable");
        table.Columns.Add("Column1", typeof(double));
        table.Columns.Add("Column2", typeof(double));
        table.Columns.Add("Column3", typeof(double));

        for (int i = 0; i < 10; i++) {
            if (i == 0)
                table.Rows.Add(null, 1.0, 1.1);
            else
               table.Rows.Add(0.0, 0.5, 2.0);
        }

        JsonSerializer serializer = new JsonSerializer();
        //serializer.TypeNameHandling = TypeNameHandling.All;
        serializer.NullValueHandling = NullValueHandling.Ignore;
        using (StreamWriter sw1 = new StreamWriter("1st.json"))
        using (JsonWriter writer1 = new JsonTextWriter(sw1))
        {
            writer1.Formatting = Formatting.Indented;
            serializer.Serialize(writer1, table);
        }

        DataTable newtable;
        using (StreamReader sr = new StreamReader("1st.json"))
        using (JsonReader reader = new JsonTextReader(sr))
        {
            newtable = (DataTable)serializer.Deserialize(reader, typeof(DataTable));
        }

        using (StreamWriter sw = new StreamWriter("3rd.json"))
        using (JsonWriter writer = new JsonTextWriter(sw))
        {
            writer.Formatting = Formatting.Indented;
            serializer.Serialize(writer, newtable);
        }
Dime
  • 41
  • 5
  • i would suggest you should create a Class and serialize/de-serialize in to that instead of DataTable if possible. – Ja9ad335h Sep 22 '15 at 21:01
  • Thanks for the suggestion, but it is not possible in the amount of time I have to convert a 100k lines of code project from XML to json, when DataTable is thrown all throughout the project (it was a horrible design, but you don't always get to choose). I will experiment with several other json libraries in the meantime. Hopefully someone has an idea.. I would just be happy with "why" it happens. By the way... the code above is an embarrassingly simple example that I wrote to verify that it is JSON.net, and not some other interaction in the codebase. A 2-d array or list would be easier. – Dime Sep 22 '15 at 21:34
  • Json.NET is open source under the [MIT license](https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md), so you can copy & modify its code. Create a version of [`DataTableConverter`](https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/DataTableConverter.cs), called e.g., `DoubleDataTableConverter`, in which `static Type GetColumnDataType(JsonReader reader)` returns `typeof(double)` for a null value. – dbc Sep 23 '15 at 05:09
  • You can use [this answer](https://stackoverflow.com/a/60463490/3189412) to pervent lost column type – Sadegh Feb 29 '20 at 09:18

1 Answers1

2

Json.NET is open source under the MIT License, so one possibility would be to create a modified version of its DataTableConverter to suit your needs. Its source code is available here.

First, create your own forked version of this class, called, say, JsonDefaultTypeDataTableConverter<T>. Modify the method GetColumnDataType to return typeof(T) for a null column value:

/// <summary>
/// Converts a <see cref="DataTable"/> to and from JSON.
/// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/DataTableConverter.cs
/// </summary>
public class JsonDefaultTypeDataTableConverter<T>  : JsonConverter
{
    private static Type GetColumnDataType(JsonReader reader)
    {
        JsonToken tokenType = reader.TokenType;

        switch (tokenType)
        {
            case JsonToken.Integer:
            case JsonToken.Boolean:
            case JsonToken.Float:
            case JsonToken.String:
            case JsonToken.Date:
            case JsonToken.Bytes:
                return reader.ValueType;
            case JsonToken.Null:
            case JsonToken.Undefined:
                return typeof(T); // WAS typeof(string).
            case JsonToken.StartArray:
                CheckedRead(reader);
                if (reader.TokenType == JsonToken.StartObject)
                {
                    return typeof (DataTable); // nested datatable
                }

                Type arrayType = GetColumnDataType(reader);
                return arrayType.MakeArrayType();
            default:
                throw new JsonSerializationException(string.Format("Unexpected JSON token when reading DataTable: {0}", tokenType));
        }
    }
}

You'll also need to fix the calls to throw a JsonSerializationException at around line 232, for instance as follows:

    private static void CheckedRead(JsonReader reader)
    {
        if (!reader.Read())
        {
            throw new JsonSerializationException(string.Format("Unexpected end when reading DataTable."));
        }
    }

And, around line 114:

        if (reader.TokenType != JsonToken.StartArray)
        {
            throw new JsonSerializationException(string.Format("Unexpected JSON token when reading DataTable. Expected StartArray, got {0}.", reader.TokenType));
        }

Now, given that you know your table contains columns of double values, you can use it like this:

        JsonSerializer serializer = new JsonSerializer();
        //serializer.TypeNameHandling = TypeNameHandling.All;
        //serializer.NullValueHandling = NullValueHandling.Ignore; -- DO NOT USE THIS OPTION.
        serializer.Converters.Add(new JsonDefaultTypeDataTableConverter<double>());

Note that, in doing this, you're not modifying the internals of Json.NET itself, you're copying and modifying one of its set of standard converters for commonly used .Net types.

Update: full version here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Here's a full version of the converter: http://pastebin.com/S79bacwe. It builds on VS 2008 or later. You don't need to rebuild all of Json.NET to use it, just add this file to your local solution. – dbc Sep 23 '15 at 17:36
  • Much thanks! That solved the problem. Do you know why it will still place Column1 as the last column if I use NullValueHandling.Ignore? I commented that out and get the expected behavior, but it'd be nice to still use that for other parts of the serialized class. – Dime Sep 23 '15 at 20:51
  • @Dime - I *think** that happens because the **written** json is missing a column, which messes up the column order when reading. You could remove the `if (serializer.NullValueHandling == NullValueHandling.Ignore && (columnValue == null || columnValue == DBNull.Value))` line from `JsonDefaultTypeDataTableConverter.WriteJson()` – dbc Sep 23 '15 at 20:57