Skip to content
BrightDish
Features Privacy Pricing Help
Features Privacy Pricing Help
Download on the App Store

Help

  • Import Web Recipes
  • Import from Paprika
  • FAQ
  • .dish File Spec
  • Contact
  • Report a Recipe

Developer reference

.dish File Format Specification

A .dish file is BrightDish’s portable recipe-exchange format. This document describes the JSON schema and the validation/sanitization rules that BrightDish applies on import, so that you can produce your own .dish files.

Versioning

This file documents the latest, current version of the BrightDish file spec. Legacy versions are not documented. BrightDish ensures backwards compatibility with older *.dish files, but you should always adopt the latest version of the spec to ensure that your recipes import as expected.

Rationale

The .dish specification exists because other recipe schemas, including those from competing apps, are poorly structured and fragile. This is particularly true for defining ingredients, where other schemas rely on implied ordering and whitespace. This specification provides consistent, anti-fragile definitions and more capabilities than competing schemas. It is derived from BrightDish's internal data models.

Note: The .dish spec is available as a markdown file for use with agents: download the spec

1. File Container

A .dish file is a single JSON document encoded as UTF-8.

PropertyValue
Extension.dish
MIME typeapplication/x-brightdishrecipe
UTIcom.incident57.BrightDish.recipeFile
Top-level structureA single JSON object representing one Recipe
CompressionOptional gzip (RFC 1952). Uncompressed JSON is also accepted.
Decompression cap50 MB maximum decompressed size
Per fileOne recipe per file

The importer auto-detects gzip via the two-byte magic number 0x1F 0x8B. If gzip-compressed data declares an ISIZE greater than 50 MB the file is rejected before decompression begins.

Note: Because .dish files are gzipped, they'll appear as binary data in text editors. To see the original JSON, append .gz to the end of the filename, then run this terminal command: gunzip [pathToFile.dish.gz].

2. Top-Level Object: Recipe

The root JSON object represents a single recipe. All fields are optional unless marked required. Unknown keys are ignored. The importer is intentionally tolerant — invalid or out-of-range values are clamped or replaced with safe defaults rather than rejecting the file.

2.1 String fields

KeyTypeRequiredMax lengthDefault if missingNotes
titleStringNo500"Untitled Recipe"Trimmed of leading/trailing whitespace.
summaryStringNo1000"No summary provided."Trimmed.
notesStringNo10,000""Trimmed. Freeform user notes.
authorStringNo250""Person/organization who created the recipe.
sourceStringNo100""Publication or origin (e.g. "Grandma", "NYT Cooking").
yieldStringNo100""Freeform (e.g. "24 cookies", "2 loaves").
servingSizeStringNo100""Freeform (e.g. "1 cup", "2 slices").
websiteStringNo—nullURL string. See sanitization below.

website URL sanitization

The importer normalizes website URLs aggressively:

  1. Trims whitespace.
  2. If the string contains no ://, prepends https://.
  3. Forces scheme to https.
  4. Strips query, user, and password components.
  5. Rejects (sets to null) if the result has no host.
"website": "https://www.example.com/recipe"

2.2 Numeric fields

All out-of-range values are clamped to the allowed range.

KeyTypeRequiredAllowed rangeDefaultNotes
ratingDoubleNo0.0 … 5.00.0
difficultyIntNo0 … 1000 means “unknown”.
prepMinutesIntNo0 … 9990
cookMinutesIntNo0 … 9990
additionalMinutesIntNo0 … 99,9990Resting, marinating, chilling, etc.
servingsCountIntNo1 … 991
calorieCountIntNo0 … 9,9990Calories per serving.

2.3 Enum fields

origin (Int)

How the recipe was originally created. Invalid values are ignored.

ValueMeaning
0Unknown
1AI — Single Cuisine
2AI — Fused Cuisines
3AI — By Ingredients
4AI — Scanned Photos
5AI — Freeform
6Manual entry
7Imported from Paprika
8Imported from a website

Third-party producers should typically use 8 (Website). Users can search for recipes from a particular origin.

meal (Int)

The temporal meal/event the recipe is associated with. Users can filter for recipes from a particular meal. Invalid values are ignored.

ValueMeaning
0Breakfast
1Lunch
2Dinner
3Snack
4Dessert
5Side dish
6Appetizer
7Soup
999Undefined / catch-all

cuisines (Array of String)

An array of cuisine identifiers associated with the recipe. Supports fusion recipes with multiple cuisines. An empty or missing array means no cuisine has been assigned.

Each entry is lowercased on import and then validated against the known cuisine values below. Strings that do not match a known value are silently dropped from the array. Duplicate entries are collapsed into a single entry so each cuisine appears at most once.

Valid values: american, argentinian, australian, brazilian, british, caribbean, chinese, colombian, creole, cuban, ethiopian, filipino, french, german, greek, indian, indonesian, italian, jamaican, japanese, korean, lebanese, mexican, moroccan, nigerian, pakistani, peruvian, polish, portuguese, russian, spanish, thai, turkish, vietnamese, other.

"cuisines": ["italian", "french"]

2.4 Identification

KeyTypeRequiredNotes
uuidStringNoRFC 4122 UUID (any version). If missing, a fresh UUID is assigned during import. Used for duplicate detection — see Section 6.

2.5 Nested object fields

KeyTypeCapNotes
ingredientSections[IngredientSection]10Preferred container for ingredients. See §3.
ingredients[Ingredient]100Alternative flat-list form. See §3 for precedence rules.
utensils[Utensil]99See §4.
steps[Step]99See §5.
nutritionInfoNutritionInfon/aSingle object. See §6.
images[RecipeImage]20See §7.
tags[Tag]50See §8.

2.6 Producer-only field

KeyTypeRequiredNotes
appBuildIntNoThe BrightDish build that produced this file. Written on export. Ignored on import (informational only).

3. Ingredients

A recipe organizes ingredients into one or more named sections (e.g. “For the Crust”, “For the Filling”). The format supports two ways of providing ingredients:

  1. Sectioned — set ingredientSections. This is what BrightDish itself exports.
  2. Flat — set ingredients instead. The importer wraps the entire flat list in a single auto-created section titled "Other Ingredients".

If a recipe contains only one ingredientSection, BrightDish presents the ingredients as a flat list (it does not display any sectioning UI). Sections will appear only if more than one exists.

Warning: If both keys are present, ingredientSections wins and ingredients is ignored.

3.1 IngredientSection object

KeyTypeRequiredCapDefaultNotes
titleStringNo200 chars"Other Ingredients"Trimmed.
sortIndexIntNo—0Ordering hint. Renumbered to 0..N on import.
ingredients[Ingredient]No100 items[]Anything past 100 is dropped.

3.2 Ingredient object

KeyTypeRequiredRangeDefaultNotes
nameStringNo150 chars"[Unnamed Ingredient]"Trimmed. Should be the noun only — quantity and unit go in their own fields.
detailsStringNo150 chars""Qualifiers shown beneath the name ("Granny Smith or Honeycrisp", "peeled and diced").
quantityDoubleNo0.0 … 999.90.0Clamped if out of range.
unitStringNo50 chars""Freeform (e.g. "tbsp", "g", "cup"). BrightDish does not interpret the unit semantically.
sortIndexIntNo—0Renumbered 0..N per section on import.

3.3 Example

{
    "ingredientSections": [
        {
            "title": "For the Crust",
            "sortIndex": 0,
            "ingredients": [
                { "name": "All-purpose flour", "quantity": 2.0, "unit": "cups", "sortIndex": 0 },
                { "name": "Butter", "details": "cold, cubed", "quantity": 0.5, "unit": "cup", "sortIndex": 1 }
            ]
        },
        {
            "title": "For the Filling",
            "sortIndex": 1,
            "ingredients": [
                { "name": "Apples", "details": "peeled and sliced", "quantity": 6.0, "unit": "", "sortIndex": 0 }
            ]
        }
    ]
}

4. Utensils

4.1 Utensil object

KeyTypeRequiredMax lengthDefaultNotes
nameStringNo150 chars"[Unnamed Utensil]"Trimmed.
sortIndexIntNo—0Renumbered 0..N on import.

Cap: 99 utensils per recipe.

4.2 Example

"utensils": [
    { "name": "9-inch pie pan", "sortIndex": 0 },
    { "name": "Rolling pin", "sortIndex": 1 }
]

5. Steps

A step is a single cooking instruction OR a section header that visually divides the directions list. A recipe may contain a maximum of 99 steps.

5.1 Step object

KeyTypeRequiredMax lengthDefaultNotes
numberIntYes——Used as the sort key. The importer renumbers all steps 1..N after sorting; the absolute value is irrelevant, only relative order matters.
titleString?No250 charsnullOptional bold heading shown above text. Required when kind is "sectionHeader".
textStringNo2000 chars"[Empty Step]"The instruction body.
kindStringNo—"step"Either "step" or "sectionHeader". See §5.2.
imageUUIDString?No—nullOptional UUID of an image in the recipe’s images array. See §7.

Warning: A Step that is missing a number property causes the entire .dish file to be rejected.

5.2 kind values

ValueMeaning
"step"A normal cooking instruction. text is shown. number displays.
"sectionHeader"A visual divider grouping nearby steps. Only title is shown. text is ignored.

5.3 Example

"steps": [
    { "number": 1, "kind": "sectionHeader", "title": "Make the dough" },
    { "number": 2, "text": "In a large bowl, whisk together the flour and salt." },
    { "number": 3, "title": "Cut in the butter", "text": "Work the butter into the flour with your fingertips until it resembles coarse crumbs.", "imageUUID": "9F3D2B7A-8C42-4E11-A6B0-5D8EFC0A1234" }
]

6. NutritionInfo

A single optional object containing per-serving nutritional values. Every field is an optional Double. A null or missing value means “unknown” and is shown dimmed in the UI; an explicit 0 is treated as a real value (zero of that nutrient).

All values are clamped to 0.0 … 9999.0. Units are fixed by field semantics; the value itself carries no unit.

6.1 Field reference

GroupKeyUnit
Fatsfatg
saturatedFatg
transFatg
unsaturatedFatg
Corecholesterolmg
sodiummg
Carbohydratescarbohydratesg
fiberg
sugarg
addedSugarg
Proteinproteing
VitaminsvitaminAmcg
vitaminB6mg
vitaminB12mcg
vitaminCmg
vitaminDmcg
vitaminEmg
vitaminKmcg
biotinmcg
cholinemg
folatemcg
niacinmg
pantothenicAcidmg
riboflavinmg
thiaminmg
Mineralscalciummg
chloridemg
chromiummcg
coppermg
iodinemcg
ironmg
magnesiummg
manganesemg
molybdenummcg
phosphorusmg
potassiummg
seleniummcg
zincmg

6.2 Example

"nutritionInfo": {
    "fat": 12.5,
    "saturatedFat": 4.2,
    "carbohydrates": 38,
    "fiber": 3,
    "sugar": 18,
    "protein": 4,
    "sodium": 220,
    "calcium": 40
}

7. Images

7.1 RecipeImage object

KeyTypeRequiredCapDefaultNotes
uuidStringNo—fresh UUIDRFC 4122 UUID. Should be stable per image so Step.imageUUID references resolve.
sortIndexIntNo—0Renumbered 0..N on import. The image with the lowest sortIndex becomes the recipe’s primary image.
captionStringNo500 chars""Trimmed.
originIntNo—0See §7.2.
imageDataString?No—nullBase64-encoded image bytes. See §7.3.

7.2 origin values

An integer that describes where an image originated. Invalid values become 0.

ValueMeaning
0Unknown
1AI-generated
2User-supplied
3Web-imported

Third-party producers should use 0 or 3.

7.3 Embedded image bytes (imageData)

imageData is a base64-encoded image payload. JPEG is the canonical and recommended format. PNG and other formats supported by Apple’s ImageIO are also accepted on import.

Per-image limits enforced on import:

LimitValue
Maximum pixel count100 megapixels (10,000 × 10,000)
Maximum dimension3,072 px on the longest side (auto-downsampled)
Encoded payload sizeBounded by the 50 MB whole-file decompression cap

Images exceeding the pixel-count cap are rejected. Images larger than 3,072 px on the longest side are downsampled at decode time.

Per-recipe cap: the importer keeps the first 20 images sorted by sortIndex and discards any extras. Any Step.imageUUID that pointed to a discarded image is cleared so no dangling references survive.

7.4 Image-Step linking

A Step may attach an image by setting imageUUID to the UUID of a RecipeImage listed in images. The reference must resolve to a uuid that survives the per-recipe cap; otherwise the importer clears it.

7.5 Example

"images": [
    {
        "uuid": "9F3D2B7A-8C42-4E11-A6B0-5D8EFC0A1234",
        "sortIndex": 0,
        "caption": "Finished pie, fresh out of the oven",
        "origin": 2,
        "imageData": "/9j/4AAQSkZJRgABAQEASABIAAD/...<truncated base64>..."
    }
]

8. Tags

User-defined freeform labels (e.g. "weeknight", "gluten-free"). A recipe may contain up to 50 tags.

8.1 Tag object

KeyTypeRequiredMax lengthDefaultNotes
nameStringYes35 chars—Trimmed. Empty (post-trim) names are skipped.
colorHexString?No9 charsnull8-character RGBA hex in Display P3 color space, with optional leading #. Examples: "5A9463FF", "#5A9463FF". Invalid → fallback color.

Note: The UI shows the tag's name in white atop a rectangle of colorHex color. Avoid values that have poor contrast with white text.

8.2 Tag deduplication on import

BrightDish performs fuzzy matching when importing tags:

  • Names are normalized by replacing dashes with spaces and lowercasing.
  • An incoming "gluten-free" matches an existing "Gluten Free" and the recipe attaches to the existing tag rather than creating a duplicate.
  • Likewise, an incoming "Gluten Free" matches an existing "gluten-free" and the recipe attaches to the existing tag rather than creating a duplicate.
  • If no match exists, the tag is created fresh.

8.3 Example

"tags": [
    { "name": "weeknight" },
    { "name": "gluten-free", "colorHex": "5A9463FF" }
]

9. Sanitization Summary

Every string field is sanitized by the importer, which:

  1. Discards leading/trailing whitespace and newlines.
  2. Truncates to the field’s maximum length (UTF-8 character count).
  3. Substitutes a default if the result is empty (where a default is documented).

Numeric fields outside their allowed range are clamped to the nearest valid value, never rejected.

Array fields exceeding their cap have the excess dropped (the first N items are kept, sorted by sortIndex/number first where applicable).

The importer is intentionally lenient — almost any malformed-but-decodable input produces a valid (if defaulted) recipe rather than a hard rejection. The exceptions:

  • Non-JSON / unparseable file bytes
  • Gzip stream declaring more than 50 MB uncompressed
  • Decompressed bytes exceeding 50 MB
  • Any Step missing the required number field

These produce a user-facing import-failure error.

10. Duplicate handling

If the importer finds an existing recipe in the user’s library with a matching uuid, it prompts the user to choose:

  • Replace existing — the existing recipe (and all its images) is deleted; the imported recipe takes its place.
  • Keep both — the imported recipe is assigned a fresh UUID and added alongside the existing one.
  • Cancel — the import aborts.

If you want your .dish files to always import as fresh recipes, generate a fresh UUID for uuid each time you produce the file.

11. Minimal Example

The smallest viable .dish file. Everything except steps[].number may be omitted.

{
    "title": "Toast",
    "summary": "Bread, made warm.",
    "ingredients": [
        { "name": "Bread", "quantity": 1, "unit": "slice" }
    ],
    "steps": [
        { "number": 1, "text": "Toast the bread in a toaster until golden brown." }
    ]
}

12. Full Example

{
    "uuid": "8B1C44A2-3F22-49DF-9E3D-1EBD5C7AAB10",
    "origin": 6,
    "title": "Apple Crumble",
    "summary": "A warm autumn dessert with cinnamon-spiced apples under a buttery oat topping.",
    "meal": 4,
    "cusines": ["american"],
    "rating": 4.5,
    "difficulty": 3,
    "prepMinutes": 20,
    "cookMinutes": 40,
    "additionalMinutes": 0,
    "servingsCount": 6,
    "yield": "1 9-inch crumble",
    "servingSize": "1 cup",
    "calorieCount": 380,
    "notes": "Best served warm with vanilla ice cream.",
    "author": "John Smith",
    "source": "Family Recipe",
    "website": "https://example.com/apple-crumble",

    "ingredientSections": [
        {
            "title": "For the Filling",
            "sortIndex": 0,
            "ingredients": [
                { "name": "Apples", "details": "peeled, cored, sliced", "quantity": 6, "unit": "", "sortIndex": 0 },
                { "name": "Brown sugar", "quantity": 0.5, "unit": "cup", "sortIndex": 1 },
                { "name": "Cinnamon", "quantity": 1, "unit": "tsp", "sortIndex": 2 },
                { "name": "Lemon juice", "quantity": 1, "unit": "tbsp", "sortIndex": 3 }
            ]
        },
        {
            "title": "For the Topping",
            "sortIndex": 1,
            "ingredients": [
                { "name": "Rolled oats", "quantity": 1, "unit": "cup", "sortIndex": 0 },
                { "name": "All-purpose flour", "quantity": 0.5, "unit": "cup", "sortIndex": 1 },
                { "name": "Brown sugar", "quantity": 0.5, "unit": "cup", "sortIndex": 2 },
                { "name": "Butter", "details": "cold, cubed", "quantity": 0.5, "unit": "cup", "sortIndex": 3 }
            ]
        }
    ],

    "utensils": [
        { "name": "9-inch baking dish", "sortIndex": 0 },
        { "name": "Mixing bowl", "sortIndex": 1 }
    ],

    "steps": [
        { "number": 1, "text": "Preheat the oven to 350°F (175°C)." },
        { "number": 2, "kind": "sectionHeader", "title": "Make the filling" },
        { "number": 3, "text": "Toss the sliced apples with brown sugar, cinnamon, and lemon juice in a large bowl." },
        { "number": 4, "text": "Transfer the apple mixture to the baking dish." },
        { "number": 5, "kind": "sectionHeader", "title": "Make the topping" },
        { "number": 6, "text": "Combine the oats, flour, and brown sugar. Cut in the cold butter until the mixture resembles coarse crumbs." },
        { "number": 7, "text": "Sprinkle the topping evenly over the apples." },
        { "number": 8, "text": "Bake for 40 minutes, until the topping is golden and the filling is bubbling." }
    ],

    "nutritionInfo": {
        "fat": 14,
        "saturatedFat": 8,
        "carbohydrates": 62,
        "fiber": 5,
        "sugar": 38,
        "protein": 3,
        "sodium": 95
    },

    "images": [
        {
            "uuid": "9F3D2B7A-8C42-4E11-A6B0-5D8EFC0A1234",
            "sortIndex": 0,
            "caption": "Fresh out of the oven",
            "origin": 2,
            "imageData": "/9j/4AAQSkZJRgABAQEASABIAAD/...<truncated>..."
        }
    ],

    "tags": [
        { "name": "dessert" },
        { "name": "weeknight", "colorHex": "5A9463FF" }
    ]
}

13. Producer Checklist

Before publishing .dish files for distribution:

  • Use UTF-8 encoding throughout.
  • Either gzip-compress the JSON (recommended for files with embedded images) or leave it uncompressed.
  • Stay under 50 MB after decompression.
  • Provide a fresh uuid per published file unless you intend duplicate detection to fire.
  • Provide every Step.number (the only truly required field).
  • Encode embedded imageData as base64-encoded JPEG; keep each image under 100 megapixels.
  • Cap your images array at 20 — anything beyond is silently dropped.
  • Stay within array caps: 10 ingredient sections, 100 ingredients per section, 99 utensils, 99 steps, 50 tags.
  • Set Step.imageUUID to the uuid of an entry in images, never to an arbitrary UUID.
BrightDish
Privacy Terms Help

© 2026 Incident 57, Inc. Made for iPhone & iPad.

Apple Intelligence, iPhone, iPad, and iCloud are trademarks of Apple Inc.