# jmap-calendars-types
Serde-annotated Rust types for JMAP Calendars ([draft-ietf-jmap-calendars-33]) and
JSCalendar ([RFC 8984]).
**Types only** — no method handlers, no async, no network I/O. Sits between
`jmap-types` (RFC 8620 primitives) and `jmap-calendars-server` /
`jmap-calendars-client`.
## What it is
| `Calendar` | `calendar` | draft §4 |
| `CalendarRights` | `calendar` | draft §4 |
| `IncludeInAvailability` | `calendar` | draft §4 |
| `CalendarEvent` | `event` | draft §5, RFC 8984 §2.1, §4, §5.1 |
| `CalendarEventFilterCondition` | `event` | draft §5.11.1 |
| `CalendarEventComparator` | `event` | draft §5.11.2 |
| `RecurrenceRule` | `jmap-jscalendar-types` | RFC 8984 §4.3.3 |
| `NDay` | `jmap-jscalendar-types` | RFC 8984 §4.3.3 |
| `Location` | `jmap-jscalendar-types` | RFC 8984 §4.2.5 |
| `VirtualLocation` | `jmap-jscalendar-types` | RFC 8984 §4.2.6 |
| `Link` | `jmap-jscalendar-types` | RFC 8984 §1.4.11 |
| `Participant` | `jmap-jscalendar-types` | RFC 8984 §4.4.6 |
| `Alert` | `jmap-jscalendar-types` | RFC 8984 §4.5.2 |
| `AlertTrigger` | `jmap-jscalendar-types` | RFC 8984 §4.5.2 |
| `OffsetTrigger` | `jmap-jscalendar-types` | RFC 8984 §4.5.2 |
| `AbsoluteTrigger` | `jmap-jscalendar-types` | RFC 8984 §4.5.2 |
| `Relation` | `jmap-jscalendar-types` | RFC 8984 §1.4.10 |
| `LocalDateTime` | `jmap-jscalendar-types` | RFC 8984 §1.4.5 |
| `Duration` | `jmap-jscalendar-types` | RFC 8984 §1.4.6 |
| `SignedDuration` | `jmap-jscalendar-types` | RFC 8984 §1.4.7 |
| `CalendarEventNotification` | `notification` | draft §7 |
| `Person` | `notification` | draft §7 |
| `NotificationType` | `notification` | draft §7 |
| `NotificationFilterCondition` | `notification` | draft §7.4.1 |
| `CalendarAlert` | `notification` | draft §7 |
| `ParticipantIdentity` | `participant_identity` | draft §3 |
| `BusyPeriod` | `availability` | draft §2.2 |
| `CalendarsCapability` | crate root | draft §1.5 |
| `CalendarsAccountCapability` | crate root | draft §1.5 |
| `CalendarsParseCapability` | crate root | draft §5.13 |
| `PrincipalCalendarsCapability` | crate root | draft §1.5 |
| `PrincipalsAvailabilityCapability` | crate root | draft §2.2 |
| `PrincipalsAvailabilityAccountCapability` | crate root | draft §2.2 |
The JSCalendar sub-object types (rows above with module = `jmap-jscalendar-types`)
live in the [`jmap-jscalendar-types`] crate and are re-exported at this crate's root
(`jmap_calendars_types::Location`). The same sub-types are consumed by
`jmap-tasks-types`.
Every public item has exactly one path in this crate: the crate root. The
Module column names the source file only; the modules are private.
[`jmap-jscalendar-types`]: https://crates.io/crates/jmap-jscalendar-types
## What it's for
draft-ietf-jmap-calendars-33 data types, consumed by
`jmap-calendars-server` (method handlers + the `CalendarsBackend` trait)
and `jmap-calendars-client` (typed method bindings). Re-exports
the `jmap-jscalendar-types` sub-objects at the crate root so callers can
opt into typed RFC 8984 JSCalendar sub-objects without taking an extra
direct dependency. Sibling to `jmap-mail-types`, `jmap-contacts-types`,
and `jmap-tasks-types` in the workspace's extension-types family.
Property enum types re-exported at the crate root:
`CalendarProperty`, `CalendarEventProperty`, `CalendarEventNotificationProperty`,
`ParticipantIdentityProperty`.
Capability URI string constants: `JMAP_CALENDARS_URI`,
`JMAP_CALENDARS_PARSE_URI`, `JMAP_PRINCIPALS_AVAILABILITY_URI`.
## Filter extensibility
Filter and comparator types in this crate — `CalendarEventFilterCondition`,
`CalendarEventComparator`, `NotificationFilterCondition`, and the generic `Filter<T>` / `Operator`
re-exported from `jmap-types` — are **intentionally not extensible** via
vendor "extras" fields. A filter clause the server does not understand
silently breaks query correctness: the client gets the wrong set of records
back with no error signal. So these types deliberately have no `extra`
catch-all field.
Vendors who need to filter on custom fields have two options:
- **IETF-track (recommended).** Use the JMAP Object Metadata extension
(`draft-ietf-jmap-metadata`, capability URI `urn:ietf:params:jmap:metadata`),
(draft-ietf-jmap-metadata-04), which adds namespace-keyed `metadata` /
`privateMetadata` properties to each listed data type, with
capability-declared schema (`dataTypes` → `namespaces` /
`supportsVendorNamespaces` / `maxDepth`, §1.2.1) and `metadataExists` /
`metadataTextContains` / `metadataTextEquals` conditions on the type's own
`/query` filter (§3.6). This is the workspace's recommended path for
vendor data that needs to be queryable. Helpers live in
[`jmap-metadata-types`](../crate-jmap-metadata-types),
[`jmap-metadata-server`](../crate-jmap-metadata-server), and
[`jmap-metadata-client`](../crate-jmap-metadata-client).
- **Pre-IETF escape.** If you cannot wait for the metadata draft, escape the
filter tree to `serde_json::Value` or fork the `FilterCondition` types.
See [`PLAN.md`](PLAN.md) in this crate for the hybrid sloppy-value pattern
this crate already uses for JSCalendar-shaped fields.
This policy is part of the workspace extras-preservation policy documented in
the workspace [`AGENTS.md`](../AGENTS.md); the filter-algebra exclusion
decision is bd JMAP-lbdy.
## Spec coverage
### draft-ietf-jmap-calendars-33
| §1.5 | `CalendarsCapability`, `CalendarsAccountCapability` |
| §2.2 | `Principal/getAvailability` — `BusyPeriod` |
| §3 | `ParticipantIdentity` |
| §4 | `Calendar`, `CalendarRights`, `IncludeInAvailability` |
| §5 | `CalendarEvent` JMAP properties (`id`, `calendarIds`, `isDraft`, `utcStart`, `utcEnd`, `useDefaultAlerts`, `blobId`, etc.) |
| §5.4 | Per-user properties (`keywords`, `color`, `freeBusyStatus`, `useDefaultAlerts`, `alerts`) |
| §5.7 | `iCalendar` wire property (ICalComponent, draft-ietf-calext-jscalendar-icalendar-28 §5.1.1) |
| §5.11.1 | `CalendarEventFilterCondition` |
| §5.11.2 | `CalendarEventComparator` |
| §5.13 | `CalendarsParseCapability` |
| §7 | `CalendarEventNotification`, `Person`, `NotificationType` |
| §7.4.1 | `NotificationFilterCondition` |
### RFC 8984 (JSCalendar)
| §1.4.5 | `LocalDateTime` newtype |
| §1.4.6 | `Duration` newtype |
| §1.4.7 | `SignedDuration` newtype |
| §1.4.10 | `Relation` |
| §1.4.11 | `Link` |
| §4.1 | Metadata properties (`uid`, `relatedTo`, `prodId`, `created`, `updated`, `sequence`) |
| §4.2 | What/where properties (`title`, `description`, `locations`, `virtualLocations`, `links`, `keywords`, `color`) |
| §4.3 | Recurrence properties (`recurrenceId`, `recurrenceRules`, `recurrenceOverrides`, `excluded`) |
| §4.3.3 | `RecurrenceRule`, `NDay` |
| §4.4 | Scheduling/sharing (`priority`, `freeBusyStatus`, `privacy`, `participants`, `replyTo`) |
| §4.4.6 | `Participant` |
| §4.5 | Alert properties (`alerts`) |
| §4.5.2 | `Alert`, `AlertTrigger`, `OffsetTrigger`, `AbsoluteTrigger` |
| §4.6 | Multilingual (`localizations`) |
| §4.7 | Time zone (`timeZone`, `timeZones`) |
| §4.7.2 | Custom time zone definitions (opaque `serde_json::Value` passthrough) |
| §5.1 | Event-specific (`start`, `duration`, `status`) |
## How to use
### Deserialize a `CalendarEvent`
```rust
use jmap_calendars_types::CalendarEvent;
let json = r#"{
"id": "ev1",
"uid": "abc-123@example.com",
"title": "Team meeting",
"start": "2024-06-15T10:00:00",
"duration": "PT1H",
"timeZone": "America/New_York",
"calendarIds": { "cal1": true },
"updated": "2024-06-01T00:00:00Z"
}"#;
let event: CalendarEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.title.as_deref(), Some("Team meeting"));
```
### Deserialize a `Calendar`
```rust
use jmap_calendars_types::Calendar;
let json = r#"{
"id": "cal1",
"name": "Work",
"color": "#4285f4",
"isSubscribed": true,
"isVisible": true,
"isDefault": false,
"includeInAvailability": "all",
"myRights": {
"mayReadFreeBusy": true,
"mayReadItems": true,
"mayWriteAll": true,
"mayWriteOwn": true,
"mayUpdatePrivate": true,
"mayRSVP": true,
"mayShare": false,
"mayDelete": false
},
"sortOrder": 0
}"#;
let cal: Calendar = serde_json::from_str(json).unwrap();
assert_eq!(cal.name, "Work");
```
### Deserialize a `BusyPeriod`
```rust
use jmap_calendars_types::BusyPeriod;
let json = r#"{
"utcStart": "2024-06-15T09:00:00Z",
"utcEnd": "2024-06-15T10:00:00Z",
"busyStatus": "busy"
}"#;
let period: BusyPeriod = serde_json::from_str(json).unwrap();
assert_eq!(period.utc_start, "2024-06-15T09:00:00Z");
```
## How it works
### `rename_all = "camelCase"`
Every struct carries `#[serde(rename_all = "camelCase")]`. Rust field names
use `snake_case`; the wire format is camelCase per JSCalendar and JMAP
conventions. The mapping is automatic: `calendar_ids` → `"calendarIds"`,
`recurrence_rules` → `"recurrenceRules"`, etc.
### `#[serde(rename = "@type")]` for JSCalendar type discriminators
JSCalendar embeds an `@type` field in sub-objects to identify their kind
(e.g. `"RecurrenceRule"`, `"Participant"`, `"Alert"`). The `@` character is
not valid in a Rust identifier, so each such field is declared as:
```rust
#[serde(rename = "@type")]
pub at_type: String,
```
`AlertTrigger` uses an internally-tagged serde enum (`#[serde(tag = "@type")]`)
so that `OffsetTrigger`, `AbsoluteTrigger`, and the forward-compatibility
`Unknown(serde_json::Value)` variant all dispatch on the same `@type` field.
### `#[non_exhaustive]` policy
All public structs and enums are `#[non_exhaustive]`. New fields may be added
as the draft evolves without breaking downstream crates. Construct structs
using `..Default::default()` or the builder pattern your crate provides.
### `serde_json::Value` for open-ended extension points
Several `CalendarEvent` fields use `Option<serde_json::Value>` rather than
concrete typed structs:
| `timeZones` | Custom VTIMEZONE definitions (RFC 8984 §4.7.2) are a complex nested format requiring a full timezone parser. Almost no caller needs to construct these. |
| `locations`, `virtualLocations`, `links`, `participants`, `alerts`, `recurrenceRules`, `excludedRecurrenceRules`, `keywords`, `categories`, `replyTo`, `relatedTo` | JSCalendar defines these as open-ended maps/arrays where values are rich nested objects. Concrete types for each are re-exported at the crate root; callers that need typed access deserialize the `Value` themselves using those types. |
The fields `recurrenceOverrides` and `localizations` use a typed envelope —
`Option<BTreeMap<String, jmap_types::PatchObject>>` — because they ARE
RFC 8620 §5.3 PatchObjects at the JMAP layer. The outer envelope is
typed; the inner `PatchObject` leaves remain `serde_json::Value` to
preserve per-leaf JSCalendar shape flexibility. Wire format is
byte-identical to a plain JSON object map via `PatchObject`'s
`#[serde(transparent)]`.
This is the correct representation, not laziness — do not change these to
typed structs without reading RFC 8984 §3.3 on extensibility.
### `iCalendar` wire name
The `icalendar` field on `CalendarEvent` carries the `iCalendar` property
(draft-ietf-jmap-calendars-33 §5.7), an ICalComponent object holding
iCalendar data that converted to the event. It is only returned when
explicitly requested. The field carries an explicit rename rather than
relying on `rename_all = "camelCase"`:
```rust
#[serde(rename = "iCalendar", skip_serializing_if = "Option::is_none")]
pub icalendar: Option<serde_json::Value>,
```
## Gotchas
- **`timeZones`, `links`, `participants`, `alerts`, `locations`,
`virtualLocations`** use `Option<serde_json::Value>` because JSCalendar
defines these as open-ended maps where values are complex nested
objects. Callers needing typed access must deserialize the `Value`
themselves using the concrete types re-exported at the crate root.
- **`recurrenceOverrides`, `localizations`** use
`Option<BTreeMap<String, jmap_types::PatchObject>>`: the outer envelope
is typed (RFC 8620 §5.3 PatchObject) while inner leaves remain `Value`
for JSCalendar shape flexibility. Construct via
`PatchObject::from_map(...)` or `Map::into()`.
- **`iCalendar: Option<Value>`** carries the ICalComponent object (jCal
arrays, RFC 7265) with no validation; the crate does not parse it.
- **draft-26 `iCalComponent`.** draft-ietf-jmap-calendars-26 used an
`iCalComponent` string. That key no longer deserializes into the typed
field; it falls into the extras catch-all and round-trips under its own
spelling. Pinned in `calendar_event_draft26_icalcomponent_lands_in_extras`.
- **Draft expired.** draft-ietf-jmap-calendars-33 is an expired Internet-Draft,
not a published RFC. Some ambiguous field semantics use best-judgment
interpretation based on the JSCalendar RFC 8984 normative base. The type
definitions will need review when (if) the draft is published.
- **`LocalDateTime` and `Duration` are unvalidated newtype wrappers** around
`String`. Format correctness (RFC 8984 §1.4.5, §1.4.6) is the backend's
responsibility; this crate does not parse the internal format.
## Crate family
```
jmap-types (RFC 8620 wire primitives)
└── jmap-calendars-types ← this crate
├── jmap-calendars-server (method handlers)
└── jmap-calendars-client (client extension trait)
```
Path dependencies between crates use `path = "../crate-jmap-*"` and will
remain that way until the family is published to crates.io.
## References
- **[draft-ietf-jmap-calendars-33]** — JMAP Calendars binding (Calendar,
CalendarEvent additions, CalendarEventNotification, ParticipantIdentity,
capability objects, error codes)
- **[RFC 8984]** — JSCalendar Event format (all JSCalendar Event properties,
RecurrenceRule, Alert, Participant, Location, etc.)
- **[RFC 8620]** — JMAP Core (request format, PatchObject, SetError,
ResultReference)
[draft-ietf-jmap-calendars-33]: https://datatracker.ietf.org/doc/draft-ietf-jmap-calendars/
[RFC 8984]: https://www.rfc-editor.org/rfc/rfc8984
[RFC 8620]: https://www.rfc-editor.org/rfc/rfc8620