C# Tip: How to JSON Serialize and Deserialize values as DateTime, DateTimeOffset, DateOnly and TimeOnly
Handling dates is never trivial. And handling JSON serialization and deserialization of dates is even more difficult. And, even worse, how can you convert a value as DateTime, DateTimeOffset, DateOnly and TimeOnly?
Table of Contents
Just a second! 🫷
If you are here, it means that you are a software developer. So, you know that storage, networking, and domain management have a cost .
If you want to support this blog, please ensure that you have disabled the adblocker for this site. I configured Google AdSense to show as few ADS as possible - I don't want to bother you with lots of ads, but I still need to add some to pay for the resources for my site.
Thank you for your understanding.
- Davide
Working with dates is often tricky. Yet, most of the time, we need to send and receive dates from APIs and databases where dates are stored as strings.
In this short article, we will see how to serialize and deserialize dates in C# using the built-in System.Text.Json library, and we will mix and match different date types, such as DateTime, DateTimeOffset, DateOnly, and TimeOnly.
Yes, I know what you are thinking: “Why have you lost your time writing an article about this topic?”. Well, I have seen many people struggling with this topic, and I have seen many questions about it on StackOverflow. So, I thought it would be useful to write an article about it.
Also, remember that this is a C# Tip, not a deep dive into the topic, but rather a quick, practical guide to help you get started with serialising and deserialising dates in C#.
For the sake of this article, I created a couple of dummy classes to use as examples:
public class DateTime_Wrapper
{
public DateTime Value { get; set; }
}
public class DateTimeOffset_Wrapper
{
public DateTimeOffset Value { get; set; }
}
public class DateOnly_Wrapper
{
public DateOnly Value { get; set; }
}
public class TimeOnly_Wrapper
{
public TimeOnly Value { get; set; }
}
Nothing special here, just simple wrapper classes to hold our date values. The only important thing is that all these classes expose a property named Value, which is the one we will serialize and deserialize in our examples. Remember to keep it public!
Afterwards, I created some dummy instances of these classes to use as examples:
var sourceDateTime = new DateTime_Wrapper
{
Value = DateTime.Parse("2024-06-01T18:34:56Z")
};
var sourceDateTimeOffset = new DateTimeOffset_Wrapper
{
Value = DateTimeOffset.Parse("2024-06-01T18:34:56Z")
};
var sourceDateOnly = new DateOnly_Wrapper
{
Value = DateOnly.Parse("2024-06-01")
};
var sourceTimeOnly = new TimeOnly_Wrapper
{
Value = TimeOnly.Parse("18:34:56")
};
Lastly, I created a JsonSerializerOptions instance to use in our examples, which allows us to have a better formatted JSON output:
var jsonFormatter = new JsonSerializerOptions
{
WriteIndented = true
};
Let’s get started!
How to serialize and deserialize C# dates when they have the same type as JSON
Luckily, round-tripping a date is really easy in C#, as all the date types are supported out of the box by System.Text.Json.
How to serialize and deserialize DateTime
DateTimes are easy to serialize and deserialize, as they are supported out of the box by System.Text.Json. You can use the JsonSerializer class to serialize and deserialize DateTime values. Here is an example:
[TestMethod]
public void DateTime_To_DateTime()
{
var json = JsonSerializer.Serialize(sourceDateTime, jsonFormatter);
var converted = JsonSerializer.Deserialize<DateTime_Wrapper>(json);
Assert.AreEqual(sourceDateTime.Value, converted.Value);
}
Nothing special here, I know. But beauty is in the tiny pieces: do you remember that DateTime uses the DateTimeKind to specify if the date is representing UTC, Local, or Undefined time zone? We discussed it in this article: C# tip: create correct DateTimes with DateTimeKind.
Well, it’s interesting to note the DateTimeKind is preserved during serialization and deserialization.
In the case of a DateTime defined like this:
var sourceDateTime = new DateTime_Wrapper
{
Value = DateTime.Parse("2024-06-01T18:34:56Z")
};
you can see how serialization and deserialization work when we specify the DateTimeKind.
sourceDateTime.Value = DateTime.SpecifyKind(sourceDateTime.Value, DateTimeKind.Unspecified);
var json = JsonSerializer.Serialize(sourceDateTime, jsonFormatter);
// the Value serialized is "2024-06-01T20:34:56"
var converted = JsonSerializer.Deserialize<DateTime_Wrapper>(json);
Assert.AreEqual(sourceDateTime.Value.Kind, converted.Value.Kind);
Then we can change the DateTimeKind to UTC, like this:
sourceDateTime.Value = DateTime.SpecifyKind(sourceDateTime.Value, DateTimeKind.Utc);
var json = JsonSerializer.Serialize(sourceDateTime, jsonFormatter);
// the Value serialized is "2024-06-01T20:34:56Z"
var converted = JsonSerializer.Deserialize<DateTime_Wrapper>(json);
Assert.AreEqual(sourceDateTime.Value.Kind, converted.Value.Kind);
or to Local, like this:
sourceDateTime.Value = DateTime.SpecifyKind(sourceDateTime.Value, DateTimeKind.Local);
var json = JsonSerializer.Serialize(sourceDateTime, jsonFormatter);
// the Value serialized is "2024-06-01T20:34:56+02:00"
var converted = JsonSerializer.Deserialize<DateTime_Wrapper>(json);
Assert.AreEqual(sourceDateTime.Value.Kind, converted.Value.Kind);
So, if you know you’ll always convert to and from DateTime, this is a good option, as it’s out of the box. But, as we will see later, it’s not always that easy.
How to serialize and deserialize DateTimeOffset
DateTimeOffset is supported out of the box by System.Text.Json too, and you can serialize and deserialize it in the same way as DateTime. Here is an example:
[TestMethod]
public void DateTimeOffset_To_DateTimeOffset()
{
var json = JsonSerializer.Serialize(sourceDateTimeOffset, jsonFormatter);
var converted = JsonSerializer.Deserialize<DateTimeOffset_Wrapper>(json);
Assert.AreEqual(sourceDateTimeOffset.Value, converted.Value);
}
Always remember that DateTimeOffset is a struct that represents a point in time relative to UTC, and it includes the offset from UTC. So, when you serialize and deserialize a DateTimeOffset, the offset information is preserved.
How to serialize and deserialize DateOnly
And the same goes for DateOnly and TimeOnly, which are also supported out of the box by System.Text.Json. Here is an example for DateOnly:
[TestMethod]
public void DateOnly_To_DateOnly()
{
var json = JsonSerializer.Serialize(sourceDateOnly, jsonFormatter);
var converted = JsonSerializer.Deserialize<DateOnly_Wrapper>(json);
Assert.AreEqual(sourceDateOnly.Value, converted.Value);
}
How to serialize and deserialize TimeOnly
And here’s an example for TimeOnly:
[TestMethod]
public void TimeOnly_To_TimeOnly()
{
var json = JsonSerializer.Serialize(sourceTimeOnly, jsonFormatter);
var converted = JsonSerializer.Deserialize<TimeOnly_Wrapper>(json);
Assert.AreEqual(sourceTimeOnly.Value, converted.Value);
}
Everything works as expected if the source and target types are the same. But what if the source and target types are different?
Serialize and deserialize to different C# date types
Now things get a bit more interesting when we want to serialize and deserialize to different types. For example, what if we want to serialize a DateTime as a DateOnly, or vice versa? Or what if we want to serialize a DateTimeOffset as a DateTime?
It’s not that obvious. Let’s see!
How to serialize and deserialize DateTimeOffset as DateTime, and vice versa
Converting from DateTime to DateTimeOffset is easy and already out of the box:
[TestMethod]
public void DateTime_To_DateTimeOffset()
{
var json = JsonSerializer.Serialize(sourceDateTime, jsonFormatter);
var converted = JsonSerializer.Deserialize<DateTimeOffset_Wrapper>(json);
Assert.AreEqual(sourceDateTime.Value, converted.Value.DateTime);
}
However, the converting from DateTimeOffset to DateTime is a little more complicated: in fact, if you try to deserialize a DateTimeOffset as a DateTime, you will get a different value, because of how time zones are handled.
[TestMethod]
public void DateTimeOffset_To_DateTime()
{
var json = JsonSerializer.Serialize(sourceDateTimeOffset, jsonFormatter);
var converted = JsonSerializer.Deserialize<DateTime_Wrapper>(json);
Assert.AreEqual(sourceDateTimeOffset.Value.DateTime, converted.Value);
// Assert.AreEqual failed. Expected:<2024-06-01 18:34:56>. Actual:<2024-06-01 20:34:56>.
}
This happens because the returned DateTime is in Unspecified kind, and the time zone information is lost during deserialization.
So, you need to use a custom converter to handle this case.
First off, you need a converter that reads a string and converts it to a DateTime, like this:
public class JsonDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var element = reader.GetString()!;
if (DateTime.TryParse(element, out DateTime res))
{
var asUtc = res.ToUniversalTime();
return asUtc;
}
throw new JsonException($"Unable to parse '{element}' as DateTime.");
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
And then you have to register the converter in the JsonSerializerOptions, like this:
[TestMethod]
public void DateTimeOffset_To_DateTime()
{
var json = JsonSerializer.Serialize(sourceDateTimeOffset, jsonFormatter);
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonDateTimeConverter()); // register the converter!
var converted = JsonSerializer.Deserialize<DateTime_Wrapper>(json, options);
Assert.AreEqual(sourceDateTimeOffset.Value.DateTime, converted.Value);
}
How to serialize and deserialize DateTimeOffset and DateTime as DateOnly, and vice versa
When serializing to and from DateOnly, you need to use a custom converter, because the built-in DateOnly deserializer only accepts “yyyy-MM-dd” strings and rejects strings that include a time portion.
Therefore, just as we saw before, you need to create a custom converter that reads a string and converts it to a DateOnly, like this:
// Converts a full ISO 8601 DateTime or DateTimeOffset string into a DateOnly value,
// discarding the time component. Required because the built-in DateOnly deserializer
// only accepts "yyyy-MM-dd" strings and rejects strings that include a time portion.
public class JsonDateOnlyConverter : JsonConverter<DateOnly>
{
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var element = reader.GetString()!;
// DateTimeOffset.TryParse handles both "2024-06-01T18:34:56Z" and
// "2024-06-01T18:34:56+00:00" formats, preserving the declared offset
// so we extract the date as-written rather than converting to local time.
if (DateTimeOffset.TryParse(element, out DateTimeOffset dto))
{
return DateOnly.FromDateTime(dto.DateTime);
}
return DateOnly.Parse(element);
}
public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
}
Once you have the converter, you can serialize and deserialize DateOnly instances as DateTime or DateTimeOffset by registering the converter in the JsonSerializerOptions:
[TestMethod]
public void DateTime_To_DateOnly()
{
var json = JsonSerializer.Serialize(sourceDateTime, jsonFormatter);
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonDateOnlyConverter());
var converted = JsonSerializer.Deserialize<DateOnly_Wrapper>(json, options);
Assert.AreEqual(DateOnly.FromDateTime(sourceDateTime.Value), converted.Value);
}
[TestMethod]
public void DateTimeOffset_To_DateOnly()
{
var json = JsonSerializer.Serialize(sourceDateTimeOffset, jsonFormatter);
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonDateOnlyConverter());
var converted = JsonSerializer.Deserialize<DateOnly_Wrapper>(json, options);
Assert.AreEqual(DateOnly.FromDateTime(sourceDateTimeOffset.Value.DateTime), converted.Value);
}
On the contrary, if you want to serialize a DateOnly as a DateTime or as a DateTimeOffset, you can just use the built-in converters, as they will ignore the time portion and set it to midnight (00:00:00), like this:
[TestMethod]
public void DateOnly_To_DateTime()
{
var json = JsonSerializer.Serialize(sourceDateOnly, jsonFormatter);
var converted = JsonSerializer.Deserialize<DateTime_Wrapper>(json);
Assert.AreEqual(sourceDateOnly.Value.ToDateTime(TimeOnly.MinValue), converted.Value);
}
[TestMethod]
public void DateOnly_To_DateTimeOffset()
{
var json = JsonSerializer.Serialize(sourceDateOnly, jsonFormatter);
var converted = JsonSerializer.Deserialize<DateTimeOffset_Wrapper>(json);
Assert.AreEqual(sourceDateOnly.Value.ToDateTime(TimeOnly.MinValue), converted.Value.DateTime);
}
How to serialize and deserialize TimeOnly as DateTimeOffset or DateTime, and vice versa
Lastly, for TimeOnly.
And this is the most difficult one: in fact, TimeOnly does not have a reference date, so you have to choose a reference date to use when converting to and from DateTime and DateTimeOffset.
Again, you will need some custom converters.
The first one is needed when you want to convert a DateTime or a DateTimeOffset to a TimeOnly, and it looks like this:
// Converts a full ISO 8601 DateTime or DateTimeOffset string into a TimeOnly value,
// discarding the date component. Required because the built-in TimeOnly deserializer
// only accepts "HH:mm:ss" strings and rejects strings that include a date portion.
public class JsonTimeOnlyConverter : JsonConverter<TimeOnly>
{
public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var element = reader.GetString()!;
// DateTimeOffset.TryParse handles both "2024-06-01T18:34:56Z" and
// "2024-06-01T18:34:56+00:00" formats. Using TimeOfDay extracts the
// time as-written without any local-timezone conversion.
if (DateTimeOffset.TryParse(element, out DateTimeOffset dto))
{
return TimeOnly.FromTimeSpan(dto.TimeOfDay);
}
return TimeOnly.Parse(element);
}
public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("HH:mm:ss", CultureInfo.InvariantCulture));
}
}
So that you can use it like this:
[TestMethod]
public void DateTime_To_TimeOnly()
{
var json = JsonSerializer.Serialize(sourceDateTime, jsonFormatter);
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonTimeOnlyConverter());
var converted = JsonSerializer.Deserialize<TimeOnly_Wrapper>(json, options);
Assert.AreEqual(TimeOnly.FromDateTime(sourceDateTime.Value), converted.Value);
}
[TestMethod]
public void DateTimeOffset_To_TimeOnly()
{
var json = JsonSerializer.Serialize(sourceDateTimeOffset, jsonFormatter);
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonTimeOnlyConverter());
var converted = JsonSerializer.Deserialize<TimeOnly_Wrapper>(json, options);
Assert.AreEqual(TimeOnly.FromDateTime(sourceDateTimeOffset.Value.DateTime), converted.Value);
}
And the second one is needed when you want to convert a TimeOnly to a DateTime.
It’s and extension of the JsonDateTimeConverter as we saw before, but it can now deal with TimeOnly inputs, using a fixed reference date (Unix epoch) to keep conversions deterministic.
public class JsonDateTimeConverter_WithTimeOnlySupport : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var element = reader.GetString()!;
// TimeOnly string like "18:34:56" — no date information, so we use a fixed
// reference date (Unix epoch) as the date component to keep conversions deterministic.
// The resulting DateTime preserves the time but the date is arbitrary;
// callers should only rely on the TimeOfDay portion.
if (TimeOnly.TryParse(element, out TimeOnly timeOnly))
{
return DateTime.UnixEpoch.Date.Add(timeOnly.ToTimeSpan());
}
if (DateTime.TryParse(element, out DateTime res))
{
var asUtc = res.ToUniversalTime();
return asUtc;
}
throw new JsonException($"Unable to parse '{element}' as DateTime.");
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
The converter can be used like this:
[TestMethod]
public void TimeOnly_To_DateTime()
{
var json = JsonSerializer.Serialize(sourceTimeOnly, jsonFormatter);
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonDateTimeConverter_WithTimeOnlySupport());
var converted = JsonSerializer.Deserialize<DateTime_Wrapper>(json, options);
Assert.AreEqual(sourceTimeOnly.Value.ToTimeSpan(), converted.Value.TimeOfDay);
}
But, if you need to convert a TimeOnly to a DateTimeOffset, you need a different converter, which looks like this:
// Converts a TimeOnly string (e.g. "18:34:56") into a DateTimeOffset, using
// the Unix epoch date as the date component to keep conversions deterministic.
// The built-in DateTimeOffset deserializer requires a full date+time string
// and will reject time-only strings.
// Note: because a TimeOnly carries no date information, the resulting DateTimeOffset
// date is effectively arbitrary — callers should only rely on the TimeOfDay portion.
public class JsonDateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var element = reader.GetString()!;
if (DateTimeOffset.TryParse(element, out DateTimeOffset res))
{
return res;
}
// TimeOnly string like "18:34:56" — combine with Unix epoch date (deterministic).
if (TimeOnly.TryParse(element, out TimeOnly timeOnly))
{
return new DateTimeOffset(DateTime.UnixEpoch.Date.Add(timeOnly.ToTimeSpan()), TimeSpan.Zero);
}
throw new JsonException($"Unable to parse '{element}' as DateTimeOffset.");
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("o"));
}
}
So that you can use it like this:
[TestMethod]
public void TimeOnly_To_DateTimeOffset()
{
var json = JsonSerializer.Serialize(sourceTimeOnly, jsonFormatter);
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonDateTimeOffsetConverter());
var converted = JsonSerializer.Deserialize<DateTimeOffset_Wrapper>(json, options);
Assert.AreEqual(sourceTimeOnly.Value.ToTimeSpan(), converted.Value.TimeOfDay);
}
Wrapping up
See? It’s not that difficult to serialize and deserialize dates in C#. You just need to know how to use the JsonSerializer class and how to create custom converters when needed.
This article first appeared on Code4IT 🐧
The only critical part is: when I have a TimeOnly and want to serialise it as a DateTime, which reference date should I use? Maybe the Unix epoch (January 1, 1970)? Or maybe the current date? Or something else?
I hope you enjoyed this article! Let's keep in touch on LinkedIn, Twitter or BlueSky! 🤜🤛
Happy coding!
🐧