Category: Design Tags: data-types, dates, times, timezones, money, currency, geospatial, identifiers, phone, email, iso-8601, geojson
YYYY-MM-DDhh:mm:ssYYYY-MM-DDThh:mm:ssZZ (UTC) or ±hh:mmEurope/LondonPT1H30MUSD, EUR, GBPemailAddress (singular) or emailAddresses (plural)stringstring type, even when the underlying type is numericAlways use string type for identifiers, regardless of the underlying type:
# OpenAPI schema
customerId:
type: string
description: "The unique identifier of the customer."
{ "customerId": "12345" }
Reason: Using string allows the server to change the underlying identifier type (e.g. integer → UUID) in the future without a breaking change to the API contract.
Security: If a server fails to parse an identifier from a string to its underlying type, it MUST return 403 Forbidden. Returning 400 Bad Request with details about the parse failure reveals the underlying type to attackers and facilitates enumeration.
| Data type | Format | Example |
|---|---|---|
| Date only | YYYY-MM-DD |
2024-07-23 |
| Time only | hh:mm:ss |
14:30:00 |
| Time with offset | hh:mm:ss±hh:mm or hh:mm:ssZ |
14:30:00+02:00, 12:30:00Z |
| DateTime (UTC) | YYYY-MM-DDThh:mm:ssZ |
2024-07-23T12:30:00Z |
| DateTime with offset | YYYY-MM-DDThh:mm:ss±hh:mm |
2024-07-23T14:30:00+02:00 |
| Named timezone | IANA timezone name | Europe/London, America/New_York |
created:
type: string
format: date
description: "The date the record was created."
{ "created": "2024-07-23" }
When both a timestamp and a named timezone are needed, use two separate fields:
scheduledAt:
type: string
format: date-time
description: "The scheduled date and time in UTC with offset."
timezone:
type: string
description: "The IANA timezone name for the scheduled time."
{
"scheduledAt": "2024-07-23T11:00:00+01:00",
"timezone": "Europe/London"
}
When storing an availability window in a local timezone (without UTC conversion):
dateTime:
type: string
format: date-time
description: "Local date-time without timezone conversion."
timeZone:
type: string
description: "IANA timezone name for interpreting the dateTime value."
{
"dateTime": "2024-07-23T12:30:00",
"timezone": "Europe/Berlin"
}
| Format | Meaning | Example |
|---|---|---|
PT{n}S |
n seconds | PT30S |
PT{n}M |
n minutes | PT5M |
PT{n}H |
n hours | PT2H |
P{n}D |
n days | P7D |
P{n}M |
n months | P3M |
P{n}Y |
n years | P1Y |
| Combined | mixed units | PT1H30M |
Exception: If the duration is always an integer of a single well-known unit, a simpler representation is acceptable:
{ "driveTimeMinutes": 5 } // integer + unit in property name
{ "driveTime": "PT5M" } // ISO 8601 preferred
{ "contractLength": "P12M" } // 12 months
Store money as an integer in the minor currency unit (e.g. cents, pence) rather than as a decimal float.
Reason: Floating-point arithmetic cannot represent decimal fractions exactly in binary:
0.1 + 0.2 = 0.30000000000000004 // floating-point rounding error
Using integers avoids rounding errors entirely. €51.22 is stored as 5122 cents.
total:
type: number
format: integer
description: "The order total in the smallest unit of the relevant currency (e.g. cents for EUR/USD)."
currency:
type: string
description: "ISO 4217 currency code."
{
"total": 5122,
"currency": "EUR"
}
When multiple money values share the same currency, place the currency code at the parent level:
{
"currency": "USD",
"subtotal": 4500,
"tax": 405,
"total": 4905
}
Currency code MAY be omitted when:
Geospatial data MUST use GeoJSON format (RFC 7946).
type:
type: string
enum: [Point]
description: "GeoJSON geometry type."
coordinates:
type: array
minItems: 2
maxItems: 2
items:
type: number
format: decimal
description: "Longitude and latitude, in that order."
{
"type": "Point",
"coordinates": [13.404954, 52.520007]
}
Note: Coordinates are [longitude, latitude] — this is the GeoJSON convention (not latitude/longitude).
{
"type": "Polygon",
"coordinates": [
[
[100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
[100.0, 1.0], [100.0, 0.0]
]
]
}
For polygons with holes, the first ring is the exterior boundary; subsequent rings are interior holes.
In query strings, a point is a comma-separated latitude,longitude string:
GET /locations/search?latlong=52.520007,13.404954
Note: In query strings, latitude comes first (opposite to GeoJSON body format).
name: latlong
in: query
description: "Latitude and longitude of the search location (lat,long)."
schema:
type: array
minItems: 2
maxItems: 2
items:
type: number
format: decimal
example: "52.520007,13.404954"
Phone numbers MUST be string type and SHOULD use E.164 format:
{ "phoneNumber": "+491234567890" }
E.164 format: + followed by country code and subscriber number, no spaces or separators. Maximum 15 digits.
Use emailAddress (not email) as the property name to avoid ambiguity with email message objects:
Single address:
{ "emailAddress": "jane.doe@example.com" }
Multiple addresses:
{
"emailAddresses": [
"jane.doe@example.com",
"john.doe@example.com"
]
}
For all other data types, follow the OpenAPI 3.1 specification:
| OpenAPI Type | Format | Description |
|---|---|---|
string |
— | General text |
string |
date |
ISO 8601 date: YYYY-MM-DD |
string |
date-time |
ISO 8601 date-time: YYYY-MM-DDThh:mm:ssZ |
string |
time |
ISO 8601 time |
string |
duration |
ISO 8601 duration |
string |
uuid |
UUID format |
string |
uri |
URI format |
string |
email |
Email address (but use emailAddress as property name) |
number |
integer |
Integer number (use for money in minor units) |
number |
float |
Single-precision float |
number |
double |
Double-precision float |
boolean |
— | true / false |
array |
— | Array of items |
object |
— | JSON object |