0

My question is very similar to this one however I don't have enough reputation to post a comment on the original answer.

I've got a custom class called FillPDF that I'm serializing on the server and deserializing on the client.

FillPDF dsPDF = JsonConvert.DeserializeObject<FillPDF>(json);

The FillPDF class consists of a DataSet property which contains a collection of DataTables

From reading the solution to the original question I'm aware of why the DateTime type is improperly being set as a String. I understand Json.Net's DataTableConverter infers each DataColumn.DataType by looking at the first row only (my first row has NULL values).

I tried to implement the solution from the original question. Dbc had suggested overriding the DataTableConverter. I've done it as such and I'm using the settings object during Serialization and Deserialization like so:

// Server
FillPDF pdfData = new FillPDF(strUniqueColID);                    
var settings = new JsonSerializerSettings { Converters = new[] { new TypeInferringDataTableConverter() } };
string json = JsonConvert.SerializeObject(pdfData, Formatting.Indented,settings);


// Client
var settings = new JsonSerializerSettings { Converters = new[] { new TypeInferringDataTableConverter() } };
FillPDF dsPDF = JsonConvert.DeserializeObject<FillPDF>(json,settings);

However I'm not getting any errors and my underlying DataTables are still not being correctly Deserialized. I assume this is because I'm serializing/deserializing a custom object as opposed to simply a DataTable like in the original question.

What I would like to be able to do is :

if(c.ColumnName.toLower().Contains("date"))
{
   // Set Column's Type to DateTime because I know all Column Names containing "date" should be of type DateTime
}

Presumably this would have to be added to the overridden TypeInferringDataTableConverter.

I'm not too sure where to go from here, so I'm turning to SO in dire need of some help!

Thanks,

Justin.

JEllery
  • 304
  • 2
  • 13
  • 1
    The problem seems to be that [`DataSetConverter`](https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/DataSetConverter.cs#L89) hardcodes using Newtonsoft's `DataTableConverter` by doing `DataTableConverter converter = new DataTableConverter();`. You may need to create your own version of `DataSetConverter` as well. – dbc Sep 21 '17 at 18:13
  • Thanks for the prompt response! I'll look into overriding the ``DataSetConverter`` – JEllery Sep 21 '17 at 18:25

1 Answers1

1

The problem seems to be that Newtonsoft's DataSetConverter hardcodes using Newtonsoft's DataTableConverter by doing DataTableConverter converter = new DataTableConverter(); and then calling its ReadJson() method directly. Thus your converter is never used.

One solution would be to create your own version of DataSetConverter as well by adapting James Newton-King's original code:

public class DataSetConverter<TDataTableConverter> : DataSetConverter where TDataTableConverter : JsonConverter, new()
{
    // This code adapted from 
    // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/DataSetConverter.cs
    // Copyright (c) 2007 James Newton-King
    // Licensed under The MIT License (MIT):
    // https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md

    readonly TDataTableConverter converter = new TDataTableConverter();

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        // handle typed datasets
        DataSet ds = (objectType == typeof(DataSet))
            ? new DataSet()
            : (DataSet)Activator.CreateInstance(objectType);

        reader.ReadAndAssert();

        while (reader.TokenType == JsonToken.PropertyName)
        {
            DataTable dt = ds.Tables[(string)reader.Value];
            bool exists = (dt != null);

            dt = (DataTable)converter.ReadJson(reader, typeof(DataTable), dt, serializer);

            if (!exists)
            {
                ds.Tables.Add(dt);
            }

            reader.ReadAndAssert();
        }

        return ds;
    }
}

public static class JsonReaderExtensions
{
    public static void ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
        {
            new JsonReaderException(string.Format("Unexpected end at path {0}", reader.Path));
        }
    }
}

Then add DataSetConverter<TypeInferringDataTableConverter> to your list of converters.

Incidentally, if all you need to do is to set the column type to DateTime when the column name includes the string "date", you might consider creating a simpler converter than TypeInferringDataTableConverter along the lines of the converter from deserialize a datatable with a missing first column:

  • Fork the code of DataTableConverter. Take note of the license at the beginning:

    // Permission is hereby granted, free of charge, to any person
    // obtaining a copy of this software and associated documentation
    // files (the "Software"), to deal in the Software without
    // restriction, including without limitation the rights to use,
    // copy, modify, merge, publish, distribute, sublicense, and/or sell
    // copies of the Software, and to permit persons to whom the
    // Software is furnished to do so, subject to the following
    // conditions:
    //
    // The above copyright notice and this permission notice shall be
    // included in all copies or substantial portions of the Software.
    //
    // ...
    
  • Have your forked converter subclass Newtonsoft's DataTableConverter; remove all code for WriteJson().

  • Modify GetColumnDataType() to pass in the column name and add the necessary logic:

    private static Type GetColumnDataType(JsonReader reader, string columnName)
    {
        JsonToken tokenType = reader.TokenType;
    
        switch (tokenType)
        {
            case JsonToken.String:
                if (columnName.IndexOf("date", StringComparison.OrdinalIgnoreCase) >= 0)
                    return typeof(DateTime);
                return reader.ValueType;
    
            case JsonToken.Integer:
            case JsonToken.Boolean:
            case JsonToken.Float:
            case JsonToken.Date:
            case JsonToken.Bytes:
                return reader.ValueType;
    
            case JsonToken.Null:
            case JsonToken.Undefined:
                if (columnName.IndexOf("date", StringComparison.OrdinalIgnoreCase) >= 0)
                    return typeof(DateTime);
                return typeof(string);
    
            case JsonToken.StartArray:
                reader.ReadAndAssert();
                if (reader.TokenType == JsonToken.StartObject)
                {
                    return typeof(DataTable); // nested datatable
                }
    
                Type arrayType = GetColumnDataType(reader, columnName);
                return arrayType.MakeArrayType();
            default:
                throw JsonSerializationException.Create(reader, "Unexpected JSON token when reading DataTable: {0}".FormatWith(CultureInfo.InvariantCulture, tokenType));
        }
    }
    
  • Then fix the call to GetColumnDataType() to pass in the column name around line 152:

    Type columnType = GetColumnDataType(reader, columnName);
    
  • Stub in any missing internal methods such as ReadAndAssert() with static extensions methods as shown here.

An alternate solution to creating your own versions of Newtonsoft's converters would be, in an [OnDeserialized] event in the container class, to loop through all columns in all tables in the DataSet and convert columns of type string (or object) whose name contains "date" to DateTime columns, using one of the answers from How To Change DataType of a DataColumn in a DataTable?.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Thanks for all your help! I've implement my own custom DataSetConverter that uses the TypeInferringDataTableConverter, seems to be working well. Once again, thanks for the prompt response yesterday and all the help you've provided! – JEllery Sep 22 '17 at 14:11