1

I'm trying to parse JSON file where dates are defined as JavaScript objects :

new Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]);

so I did try to parse it with the JavaScriptDateTimeConverter

test.json:

{"data" : [{"Date" : new Date(2015, 06, 01, 00, 00, 00)}]}

c#:

using (StreamReader file = File.OpenText(@"c:\test.json"))
{
  JsonSerializer serializer = new JsonSerializer();
  serializer.Converters.Add(new JavaScriptDateTimeConverter());
  Rootobject deserializedRoot = (Rootobject)serializer.Deserialize(file, typeof(Rootobject));
}

Unfortunatly I'm receiving this error :

Unexpected token parsing date. Expected EndConstructor, got Integer. Path 'data[0].Date1', line 13, position 30.

From my understanding JSON.Net expect at best a new Date(52231943) but doesn't handle constructor overloads of Javascript Date() object.

Is there any known way to convert the new Date(year, month, day)?

jpsimard-nyx
  • 8,587
  • 6
  • 32
  • 48
  • In the file that you are parsing, are these dates always contained within an array as above? – Mike Hixson Jul 23 '15 at 04:30
  • Please note that this is in fact not legal "json" - http://json.org. It may be a valid Javascript object, but not json. – Lasse V. Karlsen Jul 23 '15 at 18:25
  • 1
    @LasseV.Karlsen - you're right, nevertheless Json.NET supports this sort of syntax. See http://james.newtonking.com/archive/2009/02/20/good-date-times-with-json-net and http://www.newtonsoft.com/json/help/html/DatesInJSON.htm – dbc Jul 23 '15 at 18:28

1 Answers1

3

You could create your own subclass of JavaScriptDateTimeConverter.cs like so:

public class JavaScriptYMDDateTimeConverter : JavaScriptDateTimeConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        Type type = (Nullable.GetUnderlyingType(objectType) ?? objectType);
        bool isNullable = (Nullable.GetUnderlyingType(objectType) != null);

        var token = JToken.Load(reader);
        if (token == null || token.Type == JTokenType.Null)
        {
            if (!isNullable)
                throw new JsonSerializationException(string.Format("Null value for type {0} at path {1}", objectType.Name, reader.Path));
            return null;
        }
        if (token.Type != JTokenType.Constructor)
        {
            throw new JsonSerializationException(string.Format("Invalid Date constructor \"{0}\" at path {1}", token.ToString(), reader.Path));
        }
        var constructor = (JConstructor)token;
        if (!string.Equals(constructor.Name, "Date", StringComparison.Ordinal))
        {
            throw new JsonSerializationException(string.Format("Invalid Date constructor \"{0}\" at path {1}", token.ToString(), reader.Path));
        }

        var values = constructor.Values().ToArray();

        if (values.Length == 0)
        {
            throw new JsonSerializationException(string.Format("Invalid Date constructor \"{0}\" at path {1}", token.ToString(), reader.Path));
        }
        else if (values.Length == 1)
        {
            // Assume ticks
            using (var subReader = constructor.CreateReader())
            {
                while (subReader.TokenType != JsonToken.StartConstructor)
                    subReader.Read();
                return base.ReadJson(subReader, objectType, existingValue, serializer); // Use base class to convert
            }
        }
        else
        {
            var year = (values.Length > 0 ? (int)values[0] : 0);
            var month = (values.Length > 1 ? (int)values[1] : 0) + 1; // c# months go from 1 to 12, JavaScript from 0 to 11
            var day = (values.Length > 2 ? (int)values[2] : 0);
            var hour = (values.Length > 3 ? (int)values[3] : 0);
            var min = (values.Length > 4 ? (int)values[4] : 0);
            var sec = (values.Length > 5 ? (int)values[5] : 0);
            var ms = (values.Length > 6 ? (int)values[6] : 0);

            // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
            // Note: Where Date is called as a constructor with more than one argument, the specifed arguments represent local time.
            var dt = new DateTime(year, month, day, hour, min, sec, ms, DateTimeKind.Local);
            if (type == typeof(DateTimeOffset))
                return new DateTimeOffset(dt);
            return dt;
        }
    }
}

Here ReadJson() loads the token into a JConstructor, checks that the constructor name is Date, then parses the children.

Note I did not override WriteJson, so this converter will write in the same style as JavaScriptDateTimeConverter, with the ticks appearing as the single argument to the constructor.

Use it in place of JavaScriptDateTimeConverter().

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Works as expected except JToken.Load() parse leading zeros from 00 to 07 but crash with 08 and 09. I fix it upfront with : var json = Regex.Replace(input, "( )0*([1-9][0-9]*|0)", "$1$2"); – jpsimard-nyx Aug 11 '15 at 22:34
  • @lucian.jp - Glad it's working for you. Would you mind sharing an example of the JSON that causes the crash? – dbc Aug 11 '15 at 23:03
  • {"data" : [{"Date" : new Date(2015, 06, 08, 00, 00, 00)}]} – jpsimard-nyx Aug 12 '15 at 00:20
  • @lucian.jp - I found that `JsonTextReader` interprets a number starting with `0` as an **Octal**, so `08` throws an exception. (According to the [JSON standard](http://json.org/) numbers should not have a leading zero so this is an extension to the standard.) Looks like there's no workaround either: [Override Json deserializing a number with a leading zero as a decimal and not an octal value](http://stackoverflow.com/questions/29048110/override-json-deserializing-a-number-with-a-leading-zero-as-a-decimal-and-not-an). So your Regex.Replace() may be the right idea. – dbc Aug 12 '15 at 02:12
  • Strange that it works from 00 to 07 tho. Thank you anyway – jpsimard-nyx Aug 12 '15 at 02:46
  • 1
    @lucian.jp - Octal is base 8 so there's no `8` digit in Octal. – dbc Aug 12 '15 at 03:04