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:
../CANONICAL-RECIPE.md§2–§6, the shape, bounds and cross-field rules;- its JSON Schema,
CanonicalRecipe.v5.schema.json.
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. UseRecipe.recipeif 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
formatandversionfirst, so the opening line says what the file is, andimagelast, 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
imageentirely 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
sourcewith the photo, and show its credit where they show the photo's origin.
3. Two versions, kept apart
versionis the envelope: how the file wraps a recipe.recipe.schemaVersionis 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
version1,schemaVersion5 and the lowestschemaMinorthe recipe's content needs (none, for 5.0). - Readers:
- refuse a
versiongreater than they support, and say it needs a newer app; - treat a
versionthat is not an integer ≥ 1 as damaged; - refuse a
schemaVersiongreater than they support, rather than guessing; - open a newer
schemaMinorof aschemaVersionthey 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
schemaVersion4, which migrates to 5 by its version number alone (Canonical Recipe §7); - may also accept
schemaVersion1–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 localidand aname),method(steps, each with a localidandtext) andprovenance(origin:manual,website,photo,promptorimported). - Optional:
recipeDescription,yieldText;- times:
totalTimeMinutes,prepTimeMinutes,cookTimeMinutes; difficulty,cuisine,course,diets,tags;equipment;- groups:
ingredientGroupsandmethodGroups, which ingredients and steps join throughgroupId; - step
timers(durationSeconds) and stepingredients(uses, by ingredientid); tips;nutrition, with its basis, serving count and source;language(BCP 47), andschemaMinor;- in
provenance, a signed recipe'sauthorand up to threeauthorLinks,basedOn(the original a copy came from — the first in the chain), andcook_editamong itstransformations; 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
"",[]ornull. - Unknown members are invalid. The schema has
additionalProperties: falseat every level. A field Canonical Recipe 5.0 does not define cannot be added to 5.0; it needs a newschemaMinor(or, for anything that is not an optional addition, a newschemaVersion). - 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.ididentifies 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
- Refuse a file larger than you are willing to read.
- An empty file, or only whitespace, is damaged.
- 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. - If
formatis absent or is not"recipe", it is not a Recipe File. Another program may use the.recipeextension (§7). - Check
versionas in §3. Ifrecipeis not an object, the file is damaged. - Check
recipe.schemaVersionas in §3, then validate the recipe as in §4. If it is invalid, the file is damaged. - Optionally, read
imageas in §2.1. Whatever it holds, the outcome of steps 1–6 stands. - 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
authoror inauthorLinks(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 fromprovenance.urland each ofprovenance.authorLinks, without re-encoding the rest; keep a fragment the site routes on (#/…,#!…); - reduce each
transformations[].atto 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 newschemaVersion. The envelope does not change. - Optional additions to the envelope that a reader can safely ignore — like
imagein v1.1 — are minor revisions:versionstays 1, and the new member is documented here with the revision that added it. - Any other change to the envelope needs
version2 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.