JSCalendar (JSCalendar v0.1.0)

Copy Markdown View Source

JSCalendar — the JSON representation of calendar data (RFC 8984).

JSCalendar says what iCalendar says — events, tasks, recurrence, participants, time zones — in a form that does not need its own parser. It is the IETF's intended successor to iCalendar, and where iCalendar's line-folding, escaping and property parameters have produced two decades of interoperability bugs, this is a JSON object with named fields.

This library reads and writes that format. It does not schedule, and it does not expand recurrences; it turns documents into structs and back, faithfully.

iex> {:ok, event} = JSCalendar.decode(~s({
...>   "@type": "Event",
...>   "uid": "review-2026-06",
...>   "updated": "2026-06-01T09:00:00Z",
...>   "title": "Quarterly review",
...>   "start": "2026-06-02T09:00:00",
...>   "duration": "PT1H"
...> }))
iex> {event.title, event.start}
{"Quarterly review", ~N[2026-06-02 09:00:00]}

Three things it takes seriously

Unknown properties survive. Vendor extensions and later revisions of the specification both arrive as properties this library has never heard of. They are kept in each object's extra map and written back untouched, because a reader that silently drops what it does not understand corrupts data it was only asked to relay.

Defaults are not written back. A property equal to its RFC default is omitted on encode — its absence already says the same thing — so a document does not grow every time it is read and rewritten.

No dependencies. Erlang's :json and Elixir's Duration are both standard library, so nothing is pulled in to parse a document or read a duration.

What is not here yet

recurrenceOverrides, localizations and custom timeZones are preserved verbatim rather than parsed into structures — they are PatchObjects and time zone definitions whose semantics deserve their own attention rather than a hurried first pass. They round-trip correctly; they are simply not yet typed.

Summary

Types

t()

Any top-level JSCalendar object.

Functions

Read a JSCalendar document.

Write a JSCalendar object as a document.

Read a JSCalendar object from an already-decoded map.

Write a JSCalendar object as a JSON-ready map.

Types

t()

Any top-level JSCalendar object.

Functions

decode(json)

@spec decode(String.t()) :: {:ok, t()} | {:error, term()}

Read a JSCalendar document.

The object type is taken from @type, which RFC 8984 makes mandatory on every object.

Arguments

  • json is a JSCalendar document as a string.

Returns

Examples

iex> {:ok, task} = JSCalendar.decode(~s({"@type":"Task","uid":"t1","title":"Write it up"}))
iex> task.title
"Write it up"

iex> JSCalendar.decode(~s({"@type":"Sandwich"}))
{:error, {:unknown_type, "Sandwich"}}

iex> JSCalendar.decode("not json")
{:error, :invalid_json}

encode(object)

@spec encode(t()) :: {:ok, String.t()}

Write a JSCalendar object as a document.

Arguments

Returns

  • {:ok, json}.

Examples

iex> {:ok, json} = JSCalendar.encode(%JSCalendar.Event{uid: "e1"})
iex> :json.decode(json)["@type"]
"Event"

from_map(map)

@spec from_map(map()) :: {:ok, t()} | {:error, term()}

Read a JSCalendar object from an already-decoded map.

Use this when the document arrived as part of a larger payload — a JMAP response, say — and has been decoded once already.

Arguments

  • map is a map with string keys.

Returns

  • {:ok, object}; or

  • {:error, reason}.

Examples

iex> {:ok, event} = JSCalendar.from_map(%{"@type" => "Event", "uid" => "e1"})
iex> event.uid
"e1"

iex> JSCalendar.from_map(%{"uid" => "e1"})
{:error, :missing_type}

to_map(object)

@spec to_map(t()) :: map()

Write a JSCalendar object as a JSON-ready map.

Arguments

Returns

  • a map with string keys.

Examples

iex> JSCalendar.to_map(%JSCalendar.Event{uid: "e1"}) |> Map.get("uid")
"e1"