Why can I not deserialize this custom struct using Json.Net?

17,498

Solution 1

You need to write a custom JsonConverter to properly serialize and deserialize these values. Add this class to your project.

public class DateTimeWithZoneConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof (DateTimeWithZone) || objectType == typeof (DateTimeWithZone?);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var dtwz = (DateTimeWithZone) value;

        writer.WriteStartObject();
        writer.WritePropertyName("UniversalTime");
        serializer.Serialize(writer, dtwz.UniversalTime);
        writer.WritePropertyName("TimeZone");
        serializer.Serialize(writer, dtwz.TimeZone.Id);
        writer.WriteEndObject();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var ut = default(DateTime);
        var tz = default(TimeZoneInfo);
        var gotUniversalTime = false;
        var gotTimeZone = false;
        while (reader.Read())
        {
            if (reader.TokenType != JsonToken.PropertyName)
                break;

            var propertyName = (string)reader.Value;
            if (!reader.Read())
                continue;

            if (propertyName == "UniversalTime")
            {
                ut = serializer.Deserialize<DateTime>(reader);
                gotUniversalTime = true;
            }

            if (propertyName == "TimeZone")
            {
                var tzid = serializer.Deserialize<string>(reader);
                tz = TimeZoneInfo.FindSystemTimeZoneById(tzid);
                gotTimeZone = true;
            }
        }

        if (!(gotUniversalTime && gotTimeZone))
        {
            throw new InvalidDataException("An DateTimeWithZone must contain UniversalTime and TimeZone properties.");
        }

        return new DateTimeWithZone(ut, tz);
    }
}

Then register it with the json settings you're using. For example, the default settings can be changed like this:

JsonConvert.DefaultSettings = () =>
{
    var settings = new JsonSerializerSettings();
    settings.Converters.Add(new DateTimeWithZoneConverter());
    return settings;
};

Then it will properly serialize to a usable format. Example:

{
  "UniversalTime": "2014-07-13T20:24:40.4664448Z",
  "TimeZone": "Pacific Standard Time"
}

And it will deserialize properly as well.

If you want to include the local time, You would just add that to the WriteJson method, but it should probably be ignored when deserializing. Otherwise you'd have two different sources of truth. Only one can be authoritative.

Also, you might instead try Noda Time, which includes a ZonedDateTime struct for this exact purpose. There's already support for serialization via the NodaTime.Serialization.JsonNet NuGet package.

Solution 2

Just declare the constructor as follows, that's all

[JsonConstructor]
public DateTimeWithZone(DateTime universalTime, TimeZoneInfo timeZone,
                    DateTimeKind kind = DateTimeKind.Utc)
{
    universalTime = DateTime.SpecifyKind(universalTime, kind);
    _utcDateTime = universalTime.Kind != DateTimeKind.Utc
                    ? TimeZoneInfo.ConvertTimeToUtc(universalTime, timeZone)
                    : universalTime;
    _timeZone = timeZone;
}

Note: I only added JsonConstructor attribute and changed the parameter name as universalTime

Share:
17,498
MaYaN
Author by

MaYaN

Updated on July 21, 2022

Comments

  • MaYaN
    MaYaN almost 2 years

    I have a struct representing a DateTime which also has zone info as below:

    public struct DateTimeWithZone
    {
        private readonly DateTime _utcDateTime;
        private readonly TimeZoneInfo _timeZone;    
        public DateTimeWithZone(DateTime dateTime, TimeZoneInfo timeZone, 
                            DateTimeKind kind = DateTimeKind.Utc)
        {
            dateTime = DateTime.SpecifyKind(dateTime, kind);        
            _utcDateTime = dateTime.Kind != DateTimeKind.Utc 
                          ? TimeZoneInfo.ConvertTimeToUtc(dateTime, timeZone) 
                          : dateTime;
            _timeZone = timeZone;
        }    
        public DateTime UniversalTime { get { return _utcDateTime; } }
        public TimeZoneInfo TimeZone { get { return _timeZone; } }
        public DateTime LocalTime 
        { 
            get 
            { 
                return TimeZoneInfo.ConvertTime(_utcDateTime, _timeZone); 
            } 
        }
    }
    

    I can serialize the object using:

    var now = DateTime.Now;
    var dateTimeWithZone = new DateTimeWithZone(now, TimeZoneInfo.Local, DateTimeKind.Local);
    var serializedDateTimeWithZone = JsonConvert.SerializeObject(dateTimeWithZone);
    

    But when I deserialize it using the below, I get an invalid DateTime value (DateTime.MinValue)

    var deserializedDateTimeWithZone = JsonConvert.DeserializeObject<DateTimeWithZone>(serializedDateTimeWithZone);
    

    Any help is much appreciated.