Why can DateTime.MinValue not be serialized in timezones ahead of UTC?

33,527

Solution 1

The main problem is DateTime.MinValue has DateTimeKind.Unspecified kind. It is defined as:

MinValue = new DateTime(0L, DateTimeKind.Unspecified);

But this is not a real problem, this definition leads to problem during serialization. JSON DateTime serialization done through:

System.Runtime.Serialization.Json.JsonWriterDelegator.WriteDateTime(DateTime value)

Unfortunately it is defined as:

...

if (value.Kind != DateTimeKind.Utc)
{
    long num = value.Ticks - TimeZone.CurrentTimeZone.GetUtcOffset(value).Ticks;
    if ((num > DateTime.MaxValue.Ticks) || (num < DateTime.MinValue.Ticks))
    {
        throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(XmlObjectSerializer.CreateSerializationException(SR.GetString("JsonDateTimeOutOfRange"), new ArgumentOutOfRangeException("value")));
    }
}

...

So it doesn't take into account Unspecified and treats it as Local. To avoid this situation you can define your own constant:

MinValueUtc = new DateTime(0L, DateTimeKind.Utc);

or

MinValueUtc = DateTime.MinValue.ToUniversalTime();

It looks weird of course, but it helps.

Solution 2

Try to add this on any DateTime Member

[DataMember(IsRequired = false, EmitDefaultValue = false)]

Most of these erros happens because the default value of the datetime is DateTime.MinValue which is from year of 1 and the JSON serialization is from year 1970.

Solution 3

If your time zone is GMT+1, then the UTC value of DateTime.MinValue in your time zone is going to be an hour less than DateTime.MinValue.

Solution 4

using this constructor:

public DataContractJsonSerializer(Type type, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, IDataContractSurrogate dataContractSurrogate, bool alwaysEmitTypeInformation)

example code:

DataContractJsonSerializer serializer = new DataContractJsonSerializer(o.GetType(), null, int.MaxValue, false, new DateTimeSurrogate(), false);

 public class DateTimeSurrogate : IDataContractSurrogate
    {

        #region IDataContractSurrogate 成员

        public object GetCustomDataToExport(Type clrType, Type dataContractType)
        {
            return null;
        }

        public object GetCustomDataToExport(System.Reflection.MemberInfo memberInfo, Type dataContractType)
        {
            return null;
        }

        public Type GetDataContractType(Type type)
        {
            return type;
        }

        public object GetDeserializedObject(object obj, Type targetType)
        {
                   return obj;
        }

        public void GetKnownCustomDataTypes(System.Collections.ObjectModel.Collection<Type> customDataTypes)
        {

        }

        public object GetObjectToSerialize(object obj, Type targetType)
        {
            if (obj.GetType() == typeof(DateTime))
            {
                DateTime dt = (DateTime)obj;
                if (dt == DateTime.MinValue)
                {
                    dt = DateTime.MinValue.ToUniversalTime();
                    return dt;
                }
                return dt;
            }
            if (obj == null)
            {
                return null;
            }
            var q = from p in obj.GetType().GetProperties()
                    where (p.PropertyType == typeof(DateTime)) && (DateTime)p.GetValue(obj, null) == DateTime.MinValue
                    select p;
            q.ToList().ForEach(p =>
            {
                p.SetValue(obj, DateTime.MinValue.ToUniversalTime(), null);
            });
            return obj;
        }

        public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData)
        {
            return null;
        }

        public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit)
        {
            return typeDeclaration;
        }

        #endregion
    }

Solution 5

I believe a more elegant way is to instruct the serializer not to emit the default value for DateTime fields. This will save some byte during transfer and some processing when serializing for the fields that you don't have any value for them. Example:

[DataContract]
public class Document {
    [DataMember] 
    public string Title { get; set; }
    [DataMember(IsRequired = false, EmitDefaultValue = false)] 
    public DateTime Modified { get; set; } 
}

or you can use Nullables. Example:

[DataContract]
public class Document {
    [DataMember] 
    public string Title { get; set; }
    [DataMember] 
    public DateTime? Modified { get; set; } 
}

It all depends on the requirements and restrictions you might have in your project. Sometimes you cannot just change the data types. In that case you can still take advantage of DataMember attribute and keep the data types intact.

In the above example if you have new Document() { Title = "Test Document" } in the server side, when serialized to JSON it will give you {"Title": "Test Document"} so it will be easier to deal with in JavaScript or any other client in the other side of the wire. In JavaScript if you JSON.Parse() it, and try to read it, you will get back undefined. In typed languages you will have the default value for that property depending on the type (which is typically the expected behavior).

library.GetDocument(id).success(function(raw){ 
    var document = JSON.Parse(raw);
    var date = document.date; // date will be *undefined*
    ...
}
Share:
33,527
Teun D
Author by

Teun D

Updated on May 25, 2020

Comments

  • Teun D
    Teun D almost 4 years

    I am experiencing issues with a WCF REST service. The wire object that I try to return has certain properties not set, resulting in DateTime.MinValue for properties of type DateTime. The service returns an empty document (with HTTP status 200 ???). When I try to call JSON serialization myself, the exception that is thrown is:

    SerializationException: DateTime values that are greater than DateTime.MaxValue or smaller than DateTime.MinValue when converted to UTC cannot be serialized to JSON.

    This can be reproduced by running the following code in a console app:

    DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(DateTime));
    MemoryStream m = new MemoryStream();
    DateTime dt = DateTime.MinValue;
    
    // throws SerializationException in my timezone
    ser.WriteObject(m, dt);
    string json = Encoding.ASCII.GetString(m.GetBuffer());
    Console.WriteLine(json);
    

    Why is this behaviour? I think it is related to my timezone (GMT+1). As DateTime.MinValue is default(DateTime), I would expect that this can be serialized without problems.

    Any tips on how to make my REST service behave? I don't want to change my DataContract.

  • Teun D
    Teun D over 13 years
    Yes, I figured as much. But what to do? Isn't it weird that the default value of a very common class from the framework cannot be serialized in one half of the world?
  • Teun D
    Teun D over 13 years
    That is a nice explanation. This also explains why XML serialization works and JSON doesn't. My problem is that the MinValue is there because it is not set. Instead of adding a simple attribute, I will now have to set this custom MinValueUtc to all datetime properties.
  • Nick Martyshchenko
    Nick Martyshchenko over 13 years
    You may consider working (or just store) with datetimes always in UTC converting to LocalTime just before you need to show them. So after init either in constructor or just in proper setter convert 'incoming' datetime value to UTC via .ToUniversalTime(). It helps resolve default value problem.
  • Teun D
    Teun D almost 13 years
    So, where would I use this constructor? I currently just add WebMessageFormat.Json to the WebInvoke attribute. Any hints to how to combine this technique in the declaritive style?
  • Matus
    Matus over 8 years
    this didn't work for me, setting the value to new DateTime(0L, DateTimeKind.Utc); also crashes the serialization
  • Nick Martyshchenko
    Nick Martyshchenko over 8 years
    @Matus, tried again, worked as expected with .NET 4.5 / .NET 4.6. Got "\/Date(-62135596800000)\/" Do you tried code similar posted by Teun D?
  • Matus
    Matus over 8 years
    @NickMartyshchenko that's strange, with that example, the fix works, in my solution I still get an exception (although I didn't check the trace log, maybe it's a different exception)
  • Nick Martyshchenko
    Nick Martyshchenko over 8 years
    @Matus, check your exception stack trace, I think you'll have a different error. I can try to help if you share that error message.
  • Matus
    Matus over 8 years
    @NickMartyshchenko sorry for long delay, the error is the same, but the cause is different I found out, that the equality comparison doesn't consider the DateTimeKind and the values are equal even if one is Unspecified and the other is Utc so the solution works, but in my code, it was not really applied
  • Nick Martyshchenko
    Nick Martyshchenko over 8 years
    @Matus, DateTime equality comparison based on checking InternalTicks property that is raw ticks value (dateData & TicksMask) that's why DateTimeKind is ignored. Glad you can find a solution.
  • Reza
    Reza almost 8 years
    you're welcome. I improved the answer to talk about the client side as well. Someone might find it useful some day ;)
  • Tal Aloni
    Tal Aloni about 4 years
    Note that the method is called "WriteDateTimeInDefaultFormat" for a reason, see my answer on how to use the non-default method.