Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 480
MaYaN Avatar asked Jul 13 '14 19:07

MaYaN


People also ask

How do I deserialize JSON in .NET core?

A common way to deserialize JSON is to first create a class with properties and fields that represent one or more of the JSON properties. Then, to deserialize from a string or a file, call the JsonSerializer. Deserialize method.

How do you serialize and deserialize an object in C# using JSON?

It returns JSON data in string format. In Deserialization, it does the opposite of Serialization which means it converts JSON string to custom . Net object. In the following code, it calls the static method DeserializeObject() of the JsonConvert class by passing JSON data.

What does JsonConvert DeserializeObject do?

Deserializes the JSON to the specified . NET type. Deserializes the JSON to the specified . NET type using a collection of JsonConverter.


2 Answers

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

like image 57
EZI Avatar answered Oct 06 '22 00:10

EZI


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.

like image 42
Matt Johnson-Pint Avatar answered Oct 06 '22 00:10

Matt Johnson-Pint