32. Date and time
Overview
This document explains how the JSON REST API handles date and time values throughout the system, from database storage to API responses. Understanding this behavior is crucial for developers working with temporal data.
Supported Date/Time Types
The API supports three string temporal types and two numeric epoch types in schemas:
1. date
- Format:
YYYY-MM-DD - Example:
2024-01-15 - Usage: Birth dates, due dates, or any date without time component
- Database Storage: DATE column type
- JSON Output: ISO 8601 date string
2. dateTime
- Format: RFC 3339 with seconds and an explicit
Zor±HH:MMoffset - Example Input:
2024-01-15T14:30:00Z - Example Output:
2024-01-15T14:30:00.000Z - Usage: Timestamps, created/updated times, or any date with time
- Database Storage: DATETIME (MySQL) or TIMESTAMP (PostgreSQL)
- JSON Output: Full ISO 8601 datetime string with timezone
3. time
- Format:
HH:MM:SS - Example:
14:30:00 - Usage: Time of day without date context (e.g., business hours)
- Database Storage: TIME column type
- JSON Output: ISO 8601 time string
4. epochMilliseconds and epochSeconds
- Format: An integer number or canonical base-10 integer string
- Example:
1767323045000(epochMilliseconds) or1767323045(epochSeconds) - Usage: Systems whose temporal contract is explicitly a Unix epoch value
- Database Storage: BIGINT for table-backed resources
- JSON Output: Integer number in the same unit declared by the schema type
Schema Definition
The following three executable blocks form one scenario. Add them to the starting script after installing storage and before starting the server, on a fresh database. Other snippets are illustrative payloads or configuration fragments.
await api.addResource('articles', {
schema: {
title: { type: 'string', required: true },
publishedDate: { type: 'date', required: true, search: true },
createdAt: { type: 'dateTime', temporalPrecision: 3, required: true },
dailyPostTime: { type: 'time', nullable: true },
publishedEpoch: { type: 'epochMilliseconds', required: true }
},
searchSchema: {
createdBetween: { type: 'array', actualField: 'createdAt', filterOperator: 'between' }
}
})
await api.resources.articles.createKnexTable()
Date.prototype.toISOString() always emits three fractional-second digits, so
use it directly with unspecified precision or temporalPrecision: 3 (or
higher). A lower precision requires a producer that returns no more than the
configured number of digits. Validation rejects excess input precision rather
than silently truncating it.
Input Validation
On Write Operations (POST/PUT/PATCH)
The API validates and normalizes date/time inputs:
const januaryArticle = await api.resources.articles.post({
data: {
title: 'January', publishedDate: '2024-01-15',
createdAt: '2024-01-15T22:30:00+08:00', dailyPostTime: '14:30:00',
publishedEpoch: '1705329000000'
}
})
await api.resources.articles.post({
data: {
title: 'February', publishedDate: '2024-02-01',
createdAt: '2024-02-01T00:00:00Z', dailyPostTime: null,
publishedEpoch: 1706745600000
}
})
console.log(januaryArticle)
The first result retains date 2024-01-15 and time 14:30:00, normalizes
createdAt to 2024-01-15T14:30:00.000Z, and returns publishedEpoch as the number
1705329000000. Its dateTime offset describes the same instant as UTC; callers
are not required to send only Z inputs.
Accepted Input Formats:
- date: a real calendar date in
YYYY-MM-DDform - dateTime: an RFC 3339 string with seconds and an explicit timezone, such as
2024-01-15T14:30:00Zor2024-01-15T22:30:00+08:00 - time: an offset-free
HH:MM[:SS[.fraction]]string
JavaScript Date objects, SQL datetime strings, Unix timestamps, locale text, and other values accepted by Date.parse() are not valid JSON temporal inputs. Use epochMilliseconds or epochSeconds schema fields for Unix timestamps.
Storage Format:
Schema validation preserves valid JSON temporal strings exactly. The built-in storage adapters convert date and dateTime strings to database-native Date values at the database boundary, while time remains a string. A custom field-level storage.serialize function receives the validated string and owns any custom database conversion.
The built-in dateTime conversion supports millisecond precision. A value such
as 2026-09-01T10:20:30.123456Z fails with HTTP 422 before the write, because
converting it to Date would lose information. Trailing zeroes beyond three
digits are accepted (.123000Z represents exactly .123Z). To retain finer
precision in table-backed storage, use a custom storage.serialize function
and a database column and driver that preserve it. The same serializer is used
for filters and cursor comparisons, with operation: 'filter' and context: null.
Response normalization preserves fractional digits returned as strings by
storage, up to the field’s declared precision, including during conversion of
timezone offsets to UTC. It also converts valid epoch strings and bigint
values from database drivers to JSON numbers, rejecting invalid or out-of-range
epoch data.
Output Normalization
Database to API Response
All date/time values are normalized when returned from the API:
// GET /api/articles/123
{
"data": {
"type": "articles",
"id": "123",
"attributes": {
"publishedDate": "2024-01-15", // date type
"createdAt": "2024-01-15T14:30:00.000Z", // dateTime type
"updatedAt": "2024-01-15T16:45:30.000Z", // dateTime type
"dailyPostTime": "14:30:00" // time type
}
}
}
Key Normalization Behaviors:
- Boolean Normalization: Database values of
1/0are converted totrue/false - JSON Temporal Values:
date,dateTime, andtimeattributes are returned as strings that match the same public schema contract accepted on input - UTC Assumption: MySQL DATETIME values (which lack timezone info) are assumed to be UTC
Normalization runs once after the database read and again at the final response
boundary. The final pass covers getters, computed fields, query projections,
and finishing hooks that produce native Date values, including nested
relationships in plain write responses. A malformed non-null temporal value is not
reported as null; it fails with REST_API_TEMPORAL_DATA_INVALID and HTTP 500
so stored-data or enrichment defects remain visible.
Database-Specific Handling
MySQL
- Issue: DATE and DATETIME types don’t store timezone information
- Solution: The API assumes all MySQL dates are stored in UTC
- Example:
2024-01-15 14:30:00in database →2024-01-15T14:30:00.000Zin API
PostgreSQL
- Recommended: Use
TIMESTAMPTZ(timestamp with timezone) for dateTime fields - Behavior: PostgreSQL handles timezone conversion automatically
- Storage: Always stores in UTC, converts based on session timezone
Best Practices
1. Always Store in UTC
// Good: Store timestamps in UTC
const article = {
createdAt: new Date().toISOString() // "2024-01-15T14:30:00.000Z"
};
// Bad: Store in local timezone
const article = {
createdAt: new Date().toString() // "Mon Jan 15 2024 09:30:00 GMT-0500 (EST)"
};
2. Use Appropriate Types
- Use
datefor dates without time significance - Use
dateTimefor timestamps and audit fields - Use
timefor recurring daily events
3. Timezone Handling
- Send dateTime values with an explicit timezone; UTC is convenient, and valid offsets are accepted
- The API always returns dateTime values in UTC (with ‘Z’ suffix)
- Handle timezone conversion in your client application
4. Filtering and Querying
Declare public filter names in the schema or search schema, then use
queryParams.filters in programmatic calls. Temporal filter values use the
same declared field conversion as writes.
const onDate = await api.resources.articles.query({
queryParams: { filters: { publishedDate: '2024-01-15' } }
})
const inJanuary = await api.resources.articles.query({
queryParams: {
filters: { createdBetween: ['2024-01-01T00:00:00Z', '2024-01-31T23:59:59.999Z'] }
}
})
console.log(onDate.data.map(article => article.title), inJanuary.data.map(article => article.title))
Both queries return January only. between includes both boundaries. The
corresponding HTTP equality query uses the singular filter key:
GET /api/articles?filter[publishedDate]=2024-01-15
Use declared aliases such as createdBetween; arbitrary $gte/$lt objects
are not the public filter grammar. See
searching for custom ranges and
backend limits for precision rules.
Migration Considerations
From Existing Systems
If migrating from a system that stores dates differently:
- Local Time Storage: Convert all dates to UTC before importing
- String Storage: Ensure strings match expected formats
- Numeric Timestamps: Use
epochMillisecondsorepochSeconds, matching the producer’s actual unit
Database Configuration
For optimal date handling, configure your database connection:
MySQL (in Knex config):
{
client: 'mysql2',
connection: {
// ... other config
timezone: 'Z'
}
}
PostgreSQL (in Knex config):
{
client: 'pg',
connection: {
// ... other config
}
// PostgreSQL handles timezones well by default
}
Common Issues and Solutions
Issue 1: Dates Shifting by Timezone Offset
Symptom: A date like 2024-01-15 becomes 2024-01-14 or 2024-01-16
Cause: Timezone conversion during parsing
Solution: The API handles this by parsing date-only values at UTC midnight
Issue 2: MySQL Dates Appear Wrong
Symptom: Stored 14:30:00 appears as 19:30:00 or 09:30:00
Cause: MySQL DATETIME interpreted in local timezone
Solution: The API assumes MySQL dates are UTC and adds ‘Z’ suffix
Issue 3: Time Values Need Date Context
Symptom: Can’t perform date arithmetic on time-only values
Cause: Time values intentionally have no date or timezone context
Solution: Keep recurring wall-clock values as time; use dateTime when an instant is required
Technical Implementation Details
The date/time handling is implemented in two key areas:
- Input Validation (
json-rest-schema):- Validates strict JSON temporal strings on write operations
- Preserves accepted strings exactly
- Rejects JavaScript
Dateobjects and permissive legacy date formats
- Storage and Output Normalization (
storageadapters anddatabase-value-normalizers.js):- Converts validated
dateanddateTimestrings at the database boundary - Uses the same database-native representation for writes and query comparisons
- Handles database-specific quirks (MySQL timezone issues)
- Parses database date values before emitting schema-valid JSON strings
- Fixes MySQL datetime strings by assuming UTC
- Maintains consistency across different database engines
- Converts validated
This separation keeps the public JSON contract strict and serializable while still giving database drivers native values at the storage boundary.