I have an issue that I am trying to understand regarding the way that DateTimeOffset values are serialized and deserialized using DataContractJsonSerializer and Json.NET's JsonConvert.
I have the following class
[DataContract]
public class TestToSeailize
{
[DataMember]
public DateTimeOffset SaveDate { get; set; }
}
I can serialize this using DataContractJsonSerializer:
TestToSeailize item = new TestToSeailize()
{
SaveDate = new DateTimeOffset(2020 , 06, 05 , 3 ,0, 0, TimeSpan.FromHours(5))
};
DataContractJsonSerializer serializer = new DataContractJsonSerializer(item.GetType(), settings);
using (MemoryStream ms = new MemoryStream())
{
serializer.WriteObject(ms, item);
var json = Encoding.UTF8.GetString(ms.ToArray());
Console.WriteLine(json);
return json;
}
this results in the following json
{"SaveDate":{"DateTime":"\/Date(1591308000000)\/","OffsetMinutes":300}
and using Json.NET I can do the following
TestToSeailize item = new TestToSeailize()
{
SaveDate = new DateTimeOffset(2020, 06, 05, 3, 0, 0, TimeSpan.FromHours(5))
};
string json = JsonConvert.SerializeObject(item);
this results in the following json
{"SaveDate":"2020-06-05T03:00:00+05:00"}
Why do these produce different json? Is there a way I can change the DataContract Serialization to product the same json that Json.NET does?
The actual issue I am trying to solve is to get the data serialized by DataContractJsonSerializer to be deserialized by the JsonConvert.DeserialzeObject method.
The JSON generated by DataContractJsonSerializer
for DateTimeOffset
and DateTime
is as documented. From Dates/Times and JSON:
DateTimeOffset is represented in JSON as a complex type:
{"DateTime":dateTime,"OffsetMinutes":offsetMinutes}
. TheoffsetMinutes
member is the local time offset from Greenwich Mean Time (GMT), also now referred to as Coordinated Universal Time (UTC), associated with the location of the event of interest. ThedateTime
member represents the instance in time when the event of interest occurred (again, it becomes a DateTime in JavaScript when ASP.NET AJAX is in use and a string when it is not). On serialization, the dateTime member is always serialized in GMT. So, if describing 3:00 AM New York time, dateTime has a time component of 8:00 AM and offsetMinutes are 300 (minus 300 minutes or 5 hours from GMT).Note
DateTime and DateTimeOffset objects, when serialized to JSON, only preserve information to millisecond precision. Sub-millisecond values (micro/nanoseconds) are lost during serialization.
And from DateTime Wire Format:
DateTime values appear as JSON strings in the form of
"/Date(700000+0500)/"
, where the first number (700000 in the example provided) is the number of milliseconds in the GMT time zone, regular (non-daylight savings) time since midnight, January 1, 1970. The number may be negative to represent earlier times. The part that consists of "+0500" in the example is optional and indicates that the time is of the Local kind - that is, should be converted to the local time zone on deserialization. If it is absent, the time is deserialized as Utc. The actual number ("0500" in this example) and its sign (+ or -) are ignored.
For Newtonsoft see the documentation page Serializing Dates in JSON for a discussion of how it serializes dates and times. By default ISO 8601 format strings are used but several formats are supported.
Now, it is possible to customize the data contract DateTime
format by setting DataContractJsonSerializerSettings.DateTimeFormat
:
var settings = new DataContractJsonSerializerSettings
{
DateTimeFormat = new DateTimeFormat("yyyy-MM-ddTHH\\:mm\\:ss.ffFFFFFzzz", CultureInfo.InvariantCulture)
{
},
};
DataContractJsonSerializer serializer = new DataContractJsonSerializer(item.GetType(), settings);
// Remainder as in your question.
However the result for DateTimeOffset
is as follows:
{"SaveDate":{"DateTime":"2020-06-04T22:00:00.00+00:00","OffsetMinutes":300}}
Which is not the simple string you seek. There doesn't seem to be any documented way to override the serialization format for DateTimeOffset
. Demo fiddle #1 here.
Since you wrote, The actual issue I am trying to solve is to get the data serialized by DataContractJsonSerializer to be deserialized by the JsonConvert DeserialzeObject method, it will be much easier to configure Json.NET to deserialize the DataContractJsonSerializer
format. First, define the following custom JsonConverter
:
public class DataContractDateTimeOffsetConverter : JsonConverter
{
readonly bool canWrite;
public DataContractDateTimeOffsetConverter() : this(true) { }
public DataContractDateTimeOffsetConverter(bool canWrite) => this.canWrite = canWrite;
public override bool CanWrite => canWrite;
public override bool CanConvert(Type objectType) => objectType == typeof(DateTimeOffset) || objectType == typeof(DateTimeOffset?);
[JsonObject(NamingStrategyType = typeof(DefaultNamingStrategy))] // Ignore camel casing
class DateTimeOffsetDTO<TOffset> where TOffset : struct, IComparable, IFormattable
{
public DateTime DateTime { get; set; }
public TOffset OffsetMinutes { get; set; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var input = (DateTimeOffset)value;
var oldDateFormatHandling = writer.DateFormatHandling;
var oldDateTimeZoneHandling = writer.DateTimeZoneHandling;
try
{
writer.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat;
writer.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
var offsetMinutes = input.Offset.TotalMinutes;
var offsetMinutesInt = checked((int)offsetMinutes);
var dateTime = input.DateTime.AddMinutes(-input.Offset.TotalMinutes);
if (offsetMinutesInt == offsetMinutes) // An integer number of mintues
serializer.Serialize(writer, new DateTimeOffsetDTO<int> { DateTime = dateTime, OffsetMinutes = offsetMinutesInt });
else
serializer.Serialize(writer, new DateTimeOffsetDTO<double> { DateTime = dateTime, OffsetMinutes = offsetMinutes });
}
finally
{
writer.DateFormatHandling = oldDateFormatHandling;
writer.DateTimeZoneHandling = oldDateTimeZoneHandling;
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.MoveToContentAndAssert().TokenType)
{
// note that if there is a possibility of getting ISO 8601 strings for DateTimeOffset as well as complex objects, you may need to configure
// JsonSerializerSettings.DateParseHandling = DateParseHandling.None or DateParseHandling.DateTimeOffset at a higher code level to
// avoid premature deserialization as DateTime by JsonTextReader.
case JsonToken.String:
case JsonToken.Date:
return (DateTimeOffset)JToken.Load(reader);
case JsonToken.StartObject:
var old = reader.DateTimeZoneHandling;
try
{
reader.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
var dto = serializer.Deserialize<DateTimeOffsetDTO<double>>(reader);
var result = new DateTimeOffset(new DateTime(dto.DateTime.AddMinutes(dto.OffsetMinutes).Ticks, DateTimeKind.Unspecified),
TimeSpan.FromMinutes(dto.OffsetMinutes));
return result;
}
finally
{
reader.DateTimeZoneHandling = old;
}
case JsonToken.Null:
return null;
default:
throw new JsonSerializationException(); // Unknown token
}
}
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
Now you can deserialize the JSON generated by DataContractJsonSerializer
by adding the converter to JsonSerializerSettings.Converters
:
var settings = new JsonSerializerSettings
{
Converters = { new DataContractDateTimeOffsetConverter(true) },
};
var item = JsonConvert.DeserializeObject<TestToSeailize>(json, settings);
Notes:
If don't want to serialize in DataContractJsonSerializer
format, pass canWrite : false
to the converter's constructor.
If there is a possibility of getting ISO 8601 strings as well as complex objects for DateTimeOffset
values, you may need to configure JsonSerializerSettings.DateParseHandling = DateParseHandling.None
or DateParseHandling.DateTimeOffset
at a higher code level to avoid premature deserialization of ISO 8601 strings as DateTime
objects by JsonTextReader
.
Demo fiddle #2 here.