Recipe File · developer documentation

Recipe File, version 1

The complete specification of the .recipe envelope, rendered from the repository's own document. The recipe inside it is a Canonical Recipe.

Status: v1.1, 2026-10-09. Stable. Maintained alongside Chef. Changes follow §9. Since 2026-10-18 the recipe inside is Canonical Recipe 5.0, the launch format (optional additions, §4); the envelope is unchanged. v1.1 adds one optional member, image (§2.1): every v1.0 file is a v1.1 file, and a v1.0 reader opens a v1.1 file by ignoring it.

A Recipe File holds one recipe as one JSON document that a person can send to another person, and that any app can read or write with an ordinary JSON parser. The recipe inside is Canonical Recipe 5.0, defined by:

Both are normative for the payload and are not restated here. This document, with those two, is everything needed to implement the format.

Files in this folder:

File What it is
RECIPE-FILE-v1.md This specification.
recipe-file.v1.schema.json JSON Schema (2020-12) for the envelope. recipe refers to the Canonical Recipe 5.0 schema by its $id.
examples/*.recipe Valid files. Without a photo: simple, grouped, timers, provenance, complex. With one: photo (the cook's own), website-photo (credited to the site), reshared (received, then sent on), adapted (a signed recipe with two author links, changed by hand after it arrived). They contain no personal data; the photos are test images made for the purpose.

1. The file

  • One recipe per file, and at most one photograph of it, inside the same JSON (§2.1). No archives, folders, manifests or attachments, and nothing after the JSON.
  • Encoding: UTF-8 JSON (RFC 8259). The top level is an object. Writers write no byte-order mark; readers ignore one.
  • Name: the recipe's title made safe for file systems, plus .recipe. Example: Sunday Lasagne.recipe. Use Recipe.recipe if nothing usable is left.
  • Size: a recipe alone is tens of kilobytes; with a photo, typically 0.5–1.5 MB. Writers should stay under 4 MiB. Readers may refuse a file over 8 MiB.

2. The envelope

{
  "format" : "recipe",
  "version" : 1,
  "recipe" : { "schemaVersion" : 4, "id" : "…", "title" : "…", … }
}
Member Type Required Meaning
format string yes Always "recipe". This is what makes the bytes a Recipe File, whatever the file is called.
version integer yes The envelope's version: 1.
recipe object yes One Canonical Recipe (§4).
image object no One photograph of the dish (§2.1).
  • Writers put format and version first, so the opening line says what the file is, and image last, so the recipe reads from the top. Readers must not depend on member order.
  • Writers add no other top-level members in v1. Readers ignore unknown top-level members.

2.1 The photograph (v1.1)

"image" : {
  "mediaType" : "image/jpeg",
  "encoding" : "base64",
  "width" : 2560,
  "height" : 1920,
  "source" : { "kind" : "website", "credit" : "RecipeTin Eats", "url" : "https://www.recipetineats.com/lasagna/" },
  "data" : "/9j/4AAQSkZJRgABAQAAAQABAAD…"
}
Member Type Required Meaning
mediaType string yes image/jpeg (writers), or image/png.
encoding string yes base64 (RFC 4648 §4, standard alphabet, padded, no line breaks). The only encoding v1 defines.
data string yes The encoded image file.
width, height integer no The image's pixel size: a hint for layout before decoding. Readers trust the decoded image over these.
source object no Where the photograph was published, when it was not by the person sharing it.
source.kind string yes, in source website: it came from a web page. Other values may appear; readers keep or ignore them.
source.credit string no The name to credit, as a person would read it.
source.url string no The page it was published on (http or https), not the image file.

The image is an enhancement; the recipe is the contract.

  • Optional to read. A reader may ignore image entirely and must still open the recipe.
  • Never fatal. A reader that does read it, and finds it unusable — another media type or encoding, not base64, not decodable, too large — ignores it and opens the recipe without it.
  • Writers:
  • include it only when the recipe has a photograph to show;
  • write a JPEG no larger than 2560 pixels on its longest side, upright (no orientation flag), in sRGB or with its colour profile embedded;
  • keep it under 3 MiB decoded (a photo is typically 0.3–1 MB);
  • strip everything but the picture: no EXIF, GPS, TIFF, IPTC, XMP or maker data (§6).
  • Readers:
  • may refuse to decode an image over 5 MiB decoded, or wider or taller than 8192 pixels, and ignore it;
  • should keep source with the photo, and show its credit where they show the photo's origin.

3. Two versions, kept apart

  • version is the envelope: how the file wraps a recipe.
  • recipe.schemaVersion is the recipe: the Canonical Recipe contract.

Each changes on its own schedule. In envelope v1, recipe.schemaVersion is 5 (it was 4 until 2026-10-18), and recipe.schemaMinor (absent = 0) is the recipe's minor version (Canonical Recipe §2a).

  • Writers write version 1, schemaVersion 5 and the lowest schemaMinor the recipe's content needs (none, for 5.0).
  • Readers:
  • refuse a version greater than they support, and say it needs a newer app;
  • treat a version that is not an integer ≥ 1 as damaged;
  • refuse a schemaVersion greater than they support, rather than guessing;
  • open a newer schemaMinor of a schemaVersion they support by leaving out what they don't know (Canonical Recipe §2a), show it read-only, and keep or pass on the file's recipe exactly as it arrived;
  • should accept schemaVersion 4, which migrates to 5 by its version number alone (Canonical Recipe §7);
  • may also accept schemaVersion 1–3 inside an envelope, as a leniency, if they can migrate them. No conforming writer produces them.

4. The recipe

The payload must validate against Canonical Recipe 5.0 (a reader may also accept v4, which migrates to 5.0 unchanged): its JSON Schema, plus the cross-field rules in Canonical Recipe §6.

In outline:

  • Required: schemaVersion (5), id (lowercase UUID), title, servings (integer ≥ 1), ingredients (each with a local id and a name), method (steps, each with a local id and text) and provenance (origin: manual, website, photo, prompt or imported).
  • Optional:
  • recipeDescription, yieldText;
  • times: totalTimeMinutes, prepTimeMinutes, cookTimeMinutes;
  • difficulty, cuisine, course, diets, tags;
  • equipment;
  • groups: ingredientGroups and methodGroups, which ingredients and steps join through groupId;
  • step timers (durationSeconds) and step ingredients (uses, by ingredient id);
  • tips;
  • nutrition, with its basis, serving count and source;
  • language (BCP 47), and schemaMinor;
  • in provenance, a signed recipe's author and up to three authorLinks, basedOn (the original a copy came from — the first in the chain), and cook_edit among its transformations;
  • attestation, Chef's signature vouching for the credit and content (Canonical Recipe §10a).

Rules a writer must not skip:

  • Absent means unknown. Leave a value out rather than writing "", [] or null.
  • Unknown members are invalid. The schema has additionalProperties: false at every level. A field Canonical Recipe 5.0 does not define cannot be added to 5.0; it needs a new schemaMinor (or, for anything that is not an optional addition, a new schemaVersion).
  • No repair needed. Write a recipe that validates as it is. Readers may repair small slips (Canonical Recipe §6, Repairs), but a file that only opens after repair does not conform.
  • Ids stay put. recipe.id identifies the recipe across copies. A reader may use it to notice that it already holds this recipe, but must not assume it is unique in its own library. Local ids (ingredients, steps, groups, timers) are unique within the recipe, and references must resolve.

5. Reading a file

  1. Refuse a file larger than you are willing to read.
  2. An empty file, or only whitespace, is damaged.
  3. Parse it as UTF-8 JSON, ignoring a leading byte-order mark. - If parsing fails and the text began with {, treat it as damaged. - If parsing fails otherwise, or the top level is not an object, it is not a Recipe File.
  4. If format is absent or is not "recipe", it is not a Recipe File. Another program may use the .recipe extension (§7).
  5. Check version as in §3. If recipe is not an object, the file is damaged.
  6. Check recipe.schemaVersion as in §3, then validate the recipe as in §4. If it is invalid, the file is damaged.
  7. Optionally, read image as in §2.1. Whatever it holds, the outcome of steps 1–6 stands.
  8. Show the recipe to the person before keeping it. Opening a file never saves anything by itself.

Report the outcome in plain words, using three outcomes: not a recipe this app recognises, damaged, or needs a newer app. Never show JSON, schema or parser vocabulary.

6. Privacy: what must never be in the file

A Recipe File carries the recipe and where the recipe came from, and nothing about the person sending it, except, in provenance.author and provenance.authorLinks, the name and links of a writer who chose to sign the recipe.

Writers must not include:

  • account or user identifiers, including an account name, Apple ID or email address used as author or in authorLinks (a signature is only ever what its writer chose to sign with);
  • entitlements or billing;
  • the prompts or preferences a recipe was generated from;
  • notes, ratings, favourites or cooking history;
  • shopping state;
  • how the sender came to have the recipe (for example "someone shared it with me");
  • app or navigation state;
  • anything inside the photograph's file other than the picture: location, camera, lens, capture time, software, maker notes, or any other EXIF, GPS, TIFF, IPTC or XMP data.

The recipe's photograph may travel (§2.1). It is the picture the sender sees on the recipe. A photo published on a website may travel too, with its source saying where it came from; whether a writer offers that is the writer's policy, and a reader shows the credit.

Portable provenance may travel: origin, writtenBy, generator, url, siteName, author, authorLinks, basedOn, transformations. The recipe's id is the same on every copy. Writers should, before writing (and never for a recipe carrying an attestation, whose credit travels exactly as signed):

  • remove credentials, tracking parameters (utm_*, fbclid, gclid, …) and an in-page anchor from provenance.url and each of provenance.authorLinks, without re-encoding the rest; keep a fragment the site routes on (#/…, #!…);
  • reduce each transformations[].at to the day (YYYY-MM-DDT00:00:00Z).

Readers must open an author link only on the person's tap, and show its host first.

An attestation travels with the recipe, exactly as it arrived: it is an open object (only by and signature are required), and a reader keeps every member of it, known or not. A writer never adds, changes or removes one it did not receive, and never changes an attested recipe's credit. A reader shows a recipe as verified only when the attestation checks (Canonical Recipe §10a); one that doesn't, or whose form it doesn't know, is treated as absent, never as proof.

7. On a device

Value
Extension .recipe
Apple type identifier com.nathan.chef.recipe. Exported by Chef; another app imports it. It conforms to public.data and public.content, and deliberately not to public.json, so systems open it in an app rather than previewing raw text.
Media type application/x.recipe+json. This is unregistered and is used as such (RFC 6838 §3.4); a registered type would replace it.
Schema $ids Identifiers only; nothing is served at them.

Collisions. Other programs have used .recipe for unrelated files, including calibre's news recipes (Python) and AutoPkg recipes (property lists). Identify a Recipe File by its contents (§5), never by its name alone. A reader must not treat arbitrary JSON as a recipe.

There is one extension and one media type. A bare Canonical Recipe with no envelope is not a Recipe File, whatever it is called.

8. Minimal reader

read(bytes):
  if size(bytes) > 8 MiB: return DAMAGED
  text = trim(dropLeadingBOM(bytes))
  if text is empty: return DAMAGED
  doc = parseJSON(text) or return (startsWith(text, "{") ? DAMAGED : NOT_A_RECIPE_FILE)
  if doc is not object or doc.format != "recipe": return NOT_A_RECIPE_FILE
  if doc.version is not integer or doc.version < 1: return DAMAGED
  if doc.version > 1: return NEEDS_NEWER_APP
  r = doc.recipe; if r is not object: return DAMAGED
  if r.schemaVersion > 5: return NEEDS_NEWER_APP
  if r.schemaVersion == 5 and r.schemaMinor > knownMinor:
    r = reduceToKnown(r); readOnly = true      # Canonical Recipe §2a
  if not validCanonicalRecipe5(migrate(r)): return DAMAGED
  photo = readImage(doc.image)        # optional; null on any problem
  return (r, photo)

readImage(i):
  if i is not object: return null
  if i.encoding != "base64" or i.mediaType not in {"image/jpeg", "image/png"}: return null
  bytes = base64Decode(i.data) or return null
  if size(bytes) > 5 MiB or not decodable(bytes) or pixelSize(bytes) > 8192: return null
  return (bytes, i.source)

A writer is the envelope in §2 around a valid recipe, with §6 applied, and the photograph — if any — as §2.1.

9. Changing this format

  • Additions to the recipe go through Canonical Recipe's own change process (§16): an optional addition is a new schemaMinor, anything else a new schemaVersion. The envelope does not change.
  • Optional additions to the envelope that a reader can safely ignore — like image in v1.1 — are minor revisions: version stays 1, and the new member is documented here with the revision that added it.
  • Any other change to the envelope needs version 2 and a new section here. A v1 reader refuses v2 files rather than misreading them.
  • Registration. If the format is registered (a media type with IANA, a type identifier under a domain that hosts this specification), that changes the identifiers in §7, not the bytes.