2016-05-09 32 views
5

Ho un DataTable con due colonne. ShipmentDate (DateTime) e Count (Int). dopo aver deserializzato la stringa ho notato che il tipo di ShipmentDate diventa stringa se il primo valore itemarray è nullo.Il tipo di colonna DateTime diventa String type dopo aver deserializzato DataTable

Controllare l'esempio di seguito. entrambi la stringa json hanno gli stessi dati tranne il primo elemento dell'array.

string jsonTable1 = "[{\"ShipmentDate\":null,\"Count\":3},{\"ShipmentDate\":\"2015-05-13T00:00:00\",\"Count\":13},{\"ShipmentDate\":\"2015-05-19T00:00:00\",\"Count\":1},{\"ShipmentDate\":\"2015-05-26T00:00:00\",\"Count\":1},{\"ShipmentDate\":\"2015-05-28T00:00:00\",\"Count\":2}]"; 
string jsonTable2 = "[{\"ShipmentDate\":\"2015-05-13T00:00:00\",\"Count\":13},{\"ShipmentDate\":null,\"Count\":3},{\"ShipmentDate\":\"2015-05-19T00:00:00\",\"Count\":1},{\"ShipmentDate\":\"2015-05-26T00:00:00\",\"Count\":1},{\"ShipmentDate\":\"2015-05-28T00:00:00\",\"Count\":2}]"; 

DataTable tbl1 = Newtonsoft.Json.JsonConvert.DeserializeObject<DataTable>(jsonTable1); 
DataTable tbl2 = Newtonsoft.Json.JsonConvert.DeserializeObject<DataTable>(jsonTable2); 

Console.WriteLine(tbl1.Columns["ShipmentDate"].DataType); 
Console.WriteLine(tbl2.Columns["ShipmentDate"].DataType); 

Nel mio scenario ShipmentDate del primo array voce può essere nullo e crea problemi per la conversione in tipo stringa.

Ho una situazione in cui lo schema di datatable è dinamico. Non riesco a creare una classe fortemente tipizzata.

+0

Si può provare a utilizzare un oggetto fortemente tipizzato per deserializzare, anziché DataTable generico? –

+0

È possibile creare prima una classe con tipi specifici e deserializzare la stringa json in questi oggetti e successivamente convertirla in datatable. 'Newtonsoft.Json.JsonConvert.DeserializeObject (jsonTable1);' e successivamente converti in datatable come questo esempio: http: // stackoverflow.it/questions/17088779/fill-datatable-from-linq-query –

+0

@KrishnaChaithanyaMuthyala Ho dimenticato di menzionare che ho una situazione in cui lo schema di datatable è dinamico. Non riesco a creare una classe fortemente tipizzata –

risposta

5

Il problema fondamentale è che Json.NET di DataTableConverter deduce ogni DataColumn.DataType cercando in valori di token presenti nella prima fila solo. Funziona in questo modo perché esegue il flusso del JSON per la tabella anziché caricare l'intero in una gerarchia intermedia JToken. Mentre lo streaming offre prestazioni migliori con un uso ridotto della memoria, significa che i valori null nella prima riga possono causare colonne digitate in modo errato.

Questo è un problema di volta in volta in stackoverflow, ad esempio nella domanda deserialize a datatable with a missing first column. In tal caso, l'interrogante sapeva in anticipo che il tipo di colonna doveva essere double. Nel tuo caso, hai dichiarato che lo schema datatable è dinamico, quindi la risposta non può essere utilizzata. Tuttavia, come per questa domanda, poiché Json.NET è open source con lo MIT License, è possibile creare una versione modificata del suo DataTableConverter con la logica necessaria.

Come si è visto, è possibile impostare correttamente il tipo di colonna, pur mantenendo il comportamento di streaming ricordando colonne con tipi di dati ambigui, quindi sostituendo dette colonne con colonne correttamente digitato quando un tipo appropriato può essere determinato:

/// <summary> 
/// Converts a <see cref="DataTable"/> to and from JSON. 
/// </summary> 
public class TypeInferringDataTableConverter : Newtonsoft.Json.Converters.DataTableConverter 
{ 
    // Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/DataTableConverter.cs 
    // Original license: https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md 

    /// <summary> 
    /// Reads the JSON representation of the object. 
    /// </summary> 
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param> 
    /// <param name="objectType">Type of the object.</param> 
    /// <param name="existingValue">The existing value of object being read.</param> 
    /// <param name="serializer">The calling serializer.</param> 
    /// <returns>The object value.</returns> 
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
    { 
     if (reader.TokenType == JsonToken.Null) 
     { 
      return null; 
     } 

     DataTable dt = existingValue as DataTable; 

     if (dt == null) 
     { 
      // handle typed datasets 
      dt = (objectType == typeof(DataTable)) 
       ? new DataTable() 
       : (DataTable)Activator.CreateInstance(objectType); 
     } 

     // DataTable is inside a DataSet 
     // populate the name from the property name 
     if (reader.TokenType == JsonToken.PropertyName) 
     { 
      dt.TableName = (string)reader.Value; 

      reader.ReadAndAssert(); 

      if (reader.TokenType == JsonToken.Null) 
      { 
       return dt; 
      } 
     } 

     if (reader.TokenType != JsonToken.StartArray) 
     { 
      throw JsonSerializationExceptionHelper.Create(reader, "Unexpected JSON token when reading DataTable. Expected StartArray, got {0}.".FormatWith(CultureInfo.InvariantCulture, reader.TokenType)); 
     } 

     reader.ReadAndAssert(); 

     var ambiguousColumnTypes = new HashSet<string>(); 

     while (reader.TokenType != JsonToken.EndArray) 
     { 
      CreateRow(reader, dt, serializer, ambiguousColumnTypes); 

      reader.ReadAndAssert(); 
     } 

     return dt; 
    } 

    private static void CreateRow(JsonReader reader, DataTable dt, JsonSerializer serializer, HashSet<string> ambiguousColumnTypes) 
    { 
     DataRow dr = dt.NewRow(); 
     reader.ReadAndAssert(); 

     while (reader.TokenType == JsonToken.PropertyName) 
     { 
      string columnName = (string)reader.Value; 

      reader.ReadAndAssert(); 

      DataColumn column = dt.Columns[columnName]; 
      if (column == null) 
      { 
       bool isAmbiguousType; 

       Type columnType = GetColumnDataType(reader, out isAmbiguousType); 
       column = new DataColumn(columnName, columnType); 
       dt.Columns.Add(column); 

       if (isAmbiguousType) 
        ambiguousColumnTypes.Add(columnName); 
      } 
      else if (ambiguousColumnTypes.Contains(columnName)) 
      { 
       bool isAmbiguousType; 
       Type newColumnType = GetColumnDataType(reader, out isAmbiguousType); 
       if (!isAmbiguousType) 
        ambiguousColumnTypes.Remove(columnName); 
       if (newColumnType != column.DataType) 
       { 
        column = ReplaceColumn(dt, column, newColumnType, serializer); 
       } 
      } 

      if (column.DataType == typeof(DataTable)) 
      { 
       if (reader.TokenType == JsonToken.StartArray) 
       { 
        reader.ReadAndAssert(); 
       } 

       DataTable nestedDt = new DataTable(); 

       var nestedUnknownColumnTypes = new HashSet<string>(); 

       while (reader.TokenType != JsonToken.EndArray) 
       { 
        CreateRow(reader, nestedDt, serializer, nestedUnknownColumnTypes); 

        reader.ReadAndAssert(); 
       } 

       dr[columnName] = nestedDt; 
      } 
      else if (column.DataType.IsArray && column.DataType != typeof(byte[])) 
      { 
       if (reader.TokenType == JsonToken.StartArray) 
       { 
        reader.ReadAndAssert(); 
       } 

       List<object> o = new List<object>(); 

       while (reader.TokenType != JsonToken.EndArray) 
       { 
        o.Add(reader.Value); 
        reader.ReadAndAssert(); 
       } 

       Array destinationArray = Array.CreateInstance(column.DataType.GetElementType(), o.Count); 
       Array.Copy(o.ToArray(), destinationArray, o.Count); 

       dr[columnName] = destinationArray; 
      } 
      else 
      { 
       object columnValue = (reader.Value != null) 
        ? serializer.Deserialize(reader, column.DataType) ?? DBNull.Value 
        : DBNull.Value; 

       dr[columnName] = columnValue; 
      } 

      reader.ReadAndAssert(); 
     } 

     dr.EndEdit(); 
     dt.Rows.Add(dr); 
    } 

    static object RemapValue(object oldValue, Type newType, JsonSerializer serializer) 
    { 
     if (oldValue == null) 
      return null; 
     if (oldValue == DBNull.Value) 
      return oldValue; 
     return JToken.FromObject(oldValue, serializer).ToObject(newType, serializer); 
    } 

    private static DataColumn ReplaceColumn(DataTable dt, DataColumn column, Type newColumnType, JsonSerializer serializer) 
    { 
     var newValues = Enumerable.Range(0, dt.Rows.Count).Select(i => dt.Rows[i]).Select(r => RemapValue(r[column], newColumnType, serializer)).ToList(); 

     var ordinal = column.Ordinal; 
     var name = column.ColumnName; 
     var @namespace = column.Namespace; 

     var newColumn = new DataColumn(name, newColumnType); 
     newColumn.Namespace = @namespace; 
     dt.Columns.Remove(column); 
     dt.Columns.Add(newColumn); 
     newColumn.SetOrdinal(ordinal); 

     for (int i = 0; i < dt.Rows.Count; i++) 
      dt.Rows[i][newColumn] = newValues[i]; 

     return newColumn; 
    } 

    private static Type GetColumnDataType(JsonReader reader, out bool isAmbiguous) 
    { 
     JsonToken tokenType = reader.TokenType; 

     switch (tokenType) 
     { 
      case JsonToken.Integer: 
      case JsonToken.Boolean: 
      case JsonToken.Float: 
      case JsonToken.String: 
      case JsonToken.Date: 
      case JsonToken.Bytes: 
       isAmbiguous = false; 
       return reader.ValueType; 
      case JsonToken.Null: 
      case JsonToken.Undefined: 
       isAmbiguous = true; 
       return typeof(string); 
      case JsonToken.StartArray: 
       reader.ReadAndAssert(); 
       if (reader.TokenType == JsonToken.StartObject) 
       { 
        isAmbiguous = false; 
        return typeof(DataTable); // nested datatable 
       } 
       else 
       { 
        isAmbiguous = false; 
        bool innerAmbiguous; 
        // Handling ambiguity in array entries is not yet implemented because the first non-ambiguous entry in the array 
        // might occur anywhere in the sequence, requiring us to scan the entire array to determine the type, 
        // e.g., given: [null, null, null, 314, null] 
        // we would need to scan until the 314 value, and do: 
        // return typeof(Nullable<>).MakeGenericType(new[] { reader.ValueType }).MakeArrayType(); 
        Type arrayType = GetColumnDataType(reader, out innerAmbiguous); 
        return arrayType.MakeArrayType(); 
       } 
      default: 
       throw JsonSerializationExceptionHelper.Create(reader, "Unexpected JSON token when reading DataTable: {0}".FormatWith(CultureInfo.InvariantCulture, tokenType)); 
     } 
    } 
} 

internal static class JsonSerializationExceptionHelper 
{ 
    public static JsonSerializationException Create(this JsonReader reader, string format, params object[] args) 
    { 
     // Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/JsonPosition.cs 

     var lineInfo = reader as IJsonLineInfo; 
     var path = (reader == null ? null : reader.Path); 
     var message = string.Format(CultureInfo.InvariantCulture, format, args); 
     if (!message.EndsWith(Environment.NewLine, StringComparison.Ordinal)) 
     { 
      message = message.Trim(); 
      if (!message.EndsWith(".", StringComparison.Ordinal)) 
       message += "."; 
      message += " "; 
     } 
     message += string.Format(CultureInfo.InvariantCulture, "Path '{0}'", path); 
     if (lineInfo != null && lineInfo.HasLineInfo()) 
      message += string.Format(CultureInfo.InvariantCulture, ", line {0}, position {1}", lineInfo.LineNumber, lineInfo.LinePosition); 
     message += "."; 

     return new JsonSerializationException(message); 
    } 
} 

internal static class StringUtils 
{ 
    // Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/StringUtils.cs 
    public static string FormatWith(this string format, IFormatProvider provider, object arg0) 
    { 
     return format.FormatWith(provider, new[] { arg0 }); 
    } 

    private static string FormatWith(this string format, IFormatProvider provider, params object[] args) 
    { 
     return string.Format(provider, format, args); 
    } 
} 

internal static class JsonReaderExtensions 
{ 
    public static void ReadAndAssert(this JsonReader reader) 
    { 
     if (reader == null) 
      throw new ArgumentNullException("reader"); 
     if (!reader.Read()) 
     { 
      throw JsonSerializationExceptionHelper.Create(reader, "Unexpected end when reading JSON."); 
     } 
    } 
} 

poi usarlo come:

var settings = new JsonSerializerSettings { Converters = new[] { new TypeInferringDataTableConverter() } }; 

DataTable tbl1 = Newtonsoft.Json.JsonConvert.DeserializeObject<DataTable>(jsonTable1, settings); 
DataTable tbl2 = Newtonsoft.Json.JsonConvert.DeserializeObject<DataTable>(jsonTable2, settings); 

non impostare NullValueHandling = NullValueHandling.Ignore poiché i valori nulli sono ora gestiti correttamente.

prototipo fiddle

noti che, mentre questo classe gestisce ribattitura di colonne con null valori, non gestisce ribattitura di colonne contenenti valori di matrice in cui il primo elemento dell'array è nullo. Ad esempio, se la prima fila di qualche colonna ha il valore

[null, null, null, 314, null] 

Poi il tipo di colonna dedotto sarebbe idealmente typeof(long? []), tuttavia, che non viene attuata qui. Probabilmente sarebbe necessario caricare completamente il JSON in una gerarchia JToken per effettuare tale determinazione.

+0

Ero consapevole delle prime due righe della risposta. ma non sapevo come fare. Grazie per la risposta ben spiegata. –

+0

@dbc grazie, hai risparmiato un sacco di tempo –