When working with JSON as a developer, one common issue that you’ll probably come across is having to work with dates and times in JSON. JSON does not support dates, so, how to work with JSON dates then? What is the “right” JSON date format? And how to solve this in JavaScript?
JSON only understands the following data types: object, array, string, number, boolean, null. This means that you’ll have to represent your dates in one of these types, for example as a number or string. In most cases it is best to serialize dates in JSON as an ISO 8601 Date string. But there are more ways to work with dates, each with their pros and cons. In this article we’ll explore them.
It’s about conventions
First, it is important to explain that you cannot “just” select some way to serialize a date in JSON on your own. JSON is used as a data interchange format, like to send data from a server to a browser or other client. A convention only works when both sides agree and understand the convention. This means that the solution you select to model dates needs to be supported and implemented in both server and client. If you have control over both sides, it is relatively easy to align on this. If you are the provider, the server side, you have to be careful to select a solution that your clients can handle well.
Modeling date and time in JSON
There are three ways to model a date using the data types that JSON has: convert it into a number, a string, or a structured object. Each option has pros and cons, though in most cases an ISO Date is the best choice.
1. Convert the date into a unix timestamp
A unix timestamp is the number of seconds since 1 January 1970 UTC. Serializing a JavaScript date into a unix timestamp can look like this:
// Stringify a Date as a unix timestamp in seconds
// WARNING: this is a bad demo, don't copy it, you shouldn't replace native prototype methods
Date.prototype.toJSON = function () {
return this.getTime()
}
const user = {
name: 'Joe',
updated: new Date('2022-10-31T09:00:00Z')
}
console.log(JSON.stringify(user))
// {"name":"Joe","updated":1667206800000}
You can call this the “JSON timestamp format”. The nice thing about a unix timestamp is that it is very simple and straightforward. It is just a number and can easily be parsed into a Date object. There is one important choice to make though: you can either model a unix timestamp in seconds or in milliseconds, both are used in real world application, and making wrong assumptions can lead to painful bugs, so be careful (normally will spot such a 1000x bug quickly though).
Using a unix timestamp has limitations. The timestamp holds both a date and time, but doesn’t hold information about a timezone. Another drawback is that a unix timestamp isn’t readable by humans: we need a tool, either a helpful IDE or an online tool like https://tttimestamps.com to turn the number into a date and time that we can understand, which can be annoying during development.
So why would you ever model a date as a unix timestamp then? It may be necessary when other parts of your application (either frontend or backend) require working with a timestamp, and you want to align on this. Furthermore, modeling your dates as a timestamp as a number can be faster. And in a database context, it can save memory and disk space, and result in faster indexing and searching compared to the date being ISO Date string or another full-fledged class. The article The optimal way to include dates in JSON for example concludes after benchmarking that serializing as timestamp is faster than serializing as a string. Before using this as an argument though, it is important to benchmark this in your own real-world application!
2. Convert the date into an ISO 8601 Date string
In general, the best choice to model a date in JSON is as a string containing an ISO 8601 Date. The built-in JSON parser of JavaScript formats dates this way by default:
const user = {
name: 'Joe',
updated: new Date('2022-10-31T09:00:00.594Z')
}
console.log(JSON.stringify(user))
// {"name":"Joe","updated":"2022-10-31T09:00:00.594Z"}
You can call this the “JSON datetime format”. An ISO Date is a string format which can hold date, time, milliseconds and timezone, and is human-readable. In almost every case, this is the ideal solution. The official JSON schema date format is a subset of the ISO 8601, so this plays nice together too.
Only when you really need to squeeze everything out of it for the best performance, memory footprint, or data size, you may consider using for example a unix timestamp instead in your data models, in combination with more advanced serialization techniques as explained in the section about unix timestamps. In most real world applications there is no measurable performance difference though and using an ISO Date string is a perfect fit.
On a side note: the ISO 8601 Date format also supports durations. When modeling an interval, like configuring that a specific process should run once an hour, it is often tempting to model this in for example milliseconds (a numeric value, similar to a unix timestamp). Often, it is a good idea to model this as an ISO duration:
{
"refreshInterval": "PT60S",
"backupInterval": "PT24H"
}
In the above interval, we specify that an application must refresh every 60 seconds, and must run a backup once every 24 hours. This way of writing is human-readable (do you know by heart how many hours 259200000 milliseconds is?), and there is no ambiguity on whether the duration is a number in seconds or milliseconds.
3. Convert the date into a structured object
Sometimes you do not want to model date, time, and time zone information into a single property. Maybe you want a JSON date without time. Or, it can be that you only need hours and minutes and not a full datetime object. This can be modeled for example as an object holding two numbers or strings:
{
"hours": 23,
"minutes": 45
}
When working with an object, it is often a good idea to add meta information explaining with what data model we’re working with:
{
"@type": "CustomTime",
"hours": 23,
"minutes": 45
}
Adding meta information like this is very common in strict programming languages like Java/Kotlin or C#. This information is necessary when deserializing polymorphic classes, to determine which of the subclasses to instantiate.
It is also possible to model a datetime using a combination of an object, type field, and ISO Date string as follows:
{
"@type": "ISODate",
"value": "2022-10-31T09:00:00.594Z"
}
This way, it is explicit that the object holds a date. When serializing a date as just a string like "2022-10-31T09:00:00.594Z"
, it is not possible to know whether this string was originally a date, or a text field accidentally holding a date. To know that for sure, you either need an agreed-upon data model, or additional meta information like "@type": "ISODate"
.
Deserializing dates from JSON
When serializing a JSON object containing dates, the serialized dates will be represented in basic JSON data types like a number, string, or object using one of the techniques explained above. When deserializing (parsing) the document again, there is a problem: how can you know whether some string or number is supposed to be a Date? There are basically two ways to deal with this: recognize dates based on the contents or based on a model.
Deserialize dates based on data contents
Purely based on the data itself, it is quite well possible to detect whether numbers or strings contain a date.
From the three options explained before, detecting a unix timestamp is very tricky, since it can hold a number ranging from zero to more than a trillion. It is possible to write heuristics to detect a unix timestamp (either in seconds or milliseconds) assuming that we’re working with dates in, say the current decade. However, this is quite unreliable, and it is best not to rely on such a solution.
Detecting an ISO Date string is quite straightforward and safe to do. Using a regular expression, you can check whether a string holds an ISO 8601 Date, and if so, turn this into a Date object for example. In JavaScript, using JSON.parse and a reviver, this looks as follows:
function reviveDate(key, value) {
// Matches strings like "2022-08-25T09:39:19.288Z"
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
return typeof value === 'string' && isoDateRegex.test(value) ? new Date(value) : value
}
const text = '{"name":"Joe","updated":"2022-10-31T09:00:00.594Z"}'
console.log(JSON.parse(text, reviveDate))
// {
// name: 'Joe',
// updated: new Date('2022-10-31T09:00:00.594Z')
// }
Now, the regular expression used above is relatively strict, and doesn’t cover all valid cases. For example, it doesn’t recognize a time zone suffix like +08:00
. It is possible though to write a more extensive regular expression that will cover all cases.
Detecting a date using the third option, an object with a meta type field, is completely unambiguous. In JavaScript, this can look like:
function reviveDateObject(key, value) {
if (value != null && typeof value === 'object' && value['@type'] === 'ISODate') {
return new Date(value.value)
}
return value
}
const text = '{"name":"Joe","updated":{"@type":"ISODate","value":"2022-10-31T09:00:00.594Z"}}'
console.log(JSON.parse(text, reviveDateObject))
// {
// name: 'Joe',
// updated: new Date('2022-10-31T09:00:00.594Z')
// }
This works safely for arbitrary data structures. We can go a step further though when reckoning with a data model. This will offer us additional advantages, as we will see in the next section.
Deserialize dates based on a data model
Instead of looking at the contents itself to detect dates (or other dataclasses for that matter), we can specify a data model, and write a conversion function which parses and validates the document using that model. The following code example shows how this can look:
function toUser(input) {
// validate the input
if (!input || typeof input !== 'object') {
throw new TypeError('Object expected')
}
if (input.name === undefined) {
throw new TypeError('Property "name" missing')
}
if (typeof input.name !== 'string') {
throw new TypeError('Property "name" must be a string')
}
if (input.updated === undefined) {
throw new TypeError('Property "updated" missing')
}
const updated = new Date(input.updated)
if (isNaN(updated.valueOf())) {
throw new TypeError('Property "updated" contains an invalid date')
}
// return a strict, verified data model
return {
name: input.name,
updated
}
}
const text = '{"name":"Joe","updated":"2022-10-31T09:00:00.594Z"}'
const user = toUser(JSON.parse(text))
console.log(user)
// {
// name: 'Joe',
// updated: new Date('2022-10-31T09:00:00.594Z')
// }
Validating inputs like this is verbose, but results in a data model that you can trust throughout your application. There are libraries that can make it easier to write validation rules though. And for example JSON Schema is a standard that formalizes the structure of your data. In most cases it is definitely worth parsing your data into a validated model like this at the gate, it protects against odd bugs deeper down in your application if you can trust that the data is valid.
JSON date format: Conclusion
JSON does not have support for dates and times, but there are excellent solutions to serialize dates in JSON. In most cases, it is best to serialize dates as an ISO 8601 Date string: it is straightforward, unambiguous, human-readable, and well-supported. When deserializing JSON containing dates, the safest approach is to write a helper function to validate and parse the data into the expected data model.