Indice dei contenuti
La corretta gestione delle date e dei loro molteplici formati è da sempre una delle più grandi seccature per qualsiasi sviluppatore abituato a lavorare in un contesto multi-language, soprattutto quando si lavora con formati di interscambio dati basati su stringhe di testo come XML, CVS o Json. Fortunatamente per chi lavora in ambiente .NET esistono numerose librerie native e di terze parti che consentono di gestire in modo automatico la maggior parte di queste problematiche: per quanto riguarda Json la più nota - e a mio avviso la migliore - è senza ombra di dubbio Json.NET di Newtonsoft, che grazie ai suoi metodi SerializeObject e DeserializeObject consente di convertire intere classi in formato Json e viceversa senza alcuna fatica.
Per una guida all'utilizzo di quest'ottimo prodotto gratuito vi rimando alla documentazione ufficiale, ricca di esempi e demo di facile comprensione. In questo articolo, dando per scontato che sappiate già come utilizzare questo utilissimo strumento, tratterò una problematica specifica, ovvero come fare per de-serializzare correttamente una stringa di input in formato JSON contenente una o più date scritte in formati diversi tra loro, inserite come stringa vuota o comunque differenti rispetto ai formati standard.
Il Problema
Prendiamo ad esempio il seguente JSON:
1 2 3 4 5 6 |
{ "Created": "20151217", "LastModified": "2015/12/18", "Published": "", "Deleted": null } |
Come si può vedere, abbiamo una serie di situazioni piuttosto complesse da gestire: date scritte in formati diversi (yyyyMMdd e yyyy/MM/dd) e date non presenti che vengono fornite come empty string e/o come null. Ipotizziamo ora di dover de-serializzare questo JSON nella seguente classe C#:
1 2 3 4 5 6 7 8 9 10 11 |
public class MyDates { public MyDates() { } public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime? Published { get; set; } public DateTime? Deleted { get; set; } } |
Come si può vedere, la classe MyDates presenta quattro proprietà: due DateTime, relative ai due campi data sempre presenti nel JSON, e due DateTime? (o DateTime nullabili), così da gestire i campi JSON che possono essere valorizzati oppure no.
Se provassimo a utilizzare Json.NET per de-serializzare in questa classe il JSON di cui sopra, riceveremmo il seguente errore:
ERROR: System.FormatException: String was not recognized as a valid DateTime.
Il motivo è molto semplice: sia le date inserite nei rispettivi formati non-standard quanto quella presente come empty string non possono essere convertite dal DateTimeConverter predefinito, che si aspetta una stringa in formato Date(1198908717056) come possibile rappresentazione di una data.
La Soluzione
Il modo più efficace per risolvere efficacemente il problema è quello di dotarsi di un CustomDateTimeConverter, ovvero una classe che contenga delle regole precise per convertire nel modo corretto tutte le varie "tipologie" di data che contengono i JSON che ci interessa de-serializzare: formati arbitrari, empty string, valori null e potenzialmente qualsiasi altra cosa.
Quella che segue è la classe che ho messo a punto nella maggior parte dei miei progetti, progettata proprio per risolvere i problemi di cui sopra.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
using Newtonsoft.Json; using Newtonsoft.Json.Converters; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Web; /// <summary> /// Custom converter for Json.Net to handle multiple datetime string format. /// </summary> public class CustomDateTimeConverter : DateTimeConverterBase { public static string[] DefaultInputFormats = new[] { "yyyyMMdd", "yyyy/MM/dd", "dd/MM/yyyy", "dd-MM-yyyy", "yyyyMMddHHmmss", "yyyy/MM/dd HH:mm:ss", "dd/MM/yyyy HH:mm:ss", "dd-MM-yyyy HH:mm:ss" }; public static string DefaultOutputFormat = "yyyyMMdd"; public static bool DefaultEvaluateEmptyStringAsNull = true; private string[] InputFormats = DefaultInputFormats; private string OutputFormat = DefaultOutputFormat; private bool EvaluateEmptyStringAsNull = DefaultEvaluateEmptyStringAsNull; public CustomDateTimeConverter() { } public CustomDateTimeConverter(string[] inputFormats, string outputFormat, bool evaluateEmptyStringAsNull = true) { if (inputFormats != null) InputFormats = inputFormats; if (outputFormat != null) OutputFormat = outputFormat; EvaluateEmptyStringAsNull = evaluateEmptyStringAsNull; } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { string v = (reader.Value != null) ? reader.Value.ToString() : null; try { // The following line grants Nullable DateTime support. We will return (DateTime?)null if the Json property is null. if (String.IsNullOrEmpty(v) && Nullable.GetUnderlyingType(objectType) != null) { // If EvaluateEmptyStringAsNull is true an empty string will be treated as null, // otherwise we'll let DateTime.ParseExactwill throw an exception in a couple lines. if (v == null || EvaluateEmptyStringAsNull) return null; } return DateTime.ParseExact(v, InputFormats, CultureInfo.InvariantCulture, DateTimeStyles.None); } catch (Exception e) { throw new NotSupportedException(String.Format("ERROR: Input value '{0}' is not parseable using the following supported formats: {1}", v, string.Join(",", InputFormats))); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { writer.WriteValue(((DateTime)value).ToString(OutputFormat)); } } |
Una volta aggiunta questa classe al vostro progetto all'interno di un file CustomDateTimeConverter.cs potrete utilizzarla nel seguente modo:
1 |
MyDates md = JsonConvert.DeserializeObject<MyDates>(json, new CustomDateTimeConverter()); |
La classe è impostata per accettare alcuni tra i formati di input più comuni e trattare le empty string alla stregua di valori null. Nel caso in cui vogliate gestire altri formati o modificare la gestione delle stringhe vuote potete utilizzare il costruttore esteso e specificarle volta per volta oppure, se preferite, modificare le variabili statiche relative ai valori predefiniti in fase di inizializzazione della vostra applicazione.
Per il momento è tutto... felice (de)serializzazione!