Recipe File Formats Compared: A Developer's Guide to Cooklang, JSON-LD, RecipeML, and MealMaster
If you're building a recipe app, a meal planner, a grocery integration, or anything that ingests or stores recipe data, you need to pick a format — or at least understand the formats you'll encounter. There are four that actually matter: Cooklang, JSON-LD with Schema.org vocabulary, RecipeML, and MealMaster. Each one comes from a different era and makes different trade-offs between human readability, machine parseability, and available tooling.
This post shows the same recipe — a simple tomato pasta — in all four formats, then compares them at the code level. No abstractions. Just what the bytes look like and what that means for your parser.
The Same Recipe in Four Formats
Cooklang
JSON-LD / Schema.org
MealMaster
RecipeML
Format-by-Format Analysis
Cooklang
Ingredient representation. Ingredients are annotated inline with @name{quantity%unit}. Parsing the ingredient @canned tomatoes{400%g} gives you name: "canned tomatoes", quantity: "400", unit: "g" — structured and typed, no string parsing required. The ingredient list the parser returns is derived from the prose, not maintained separately.
Parser availability. This is where Cooklang stands apart. There are 15+ independent implementations: cooklang-rs (Rust, the reference parser), cooklang-ts (TypeScript), plus community implementations in Python, Go, Swift, Haskell, Dart, Clojure, Lua, Perl, Ruby, .NET, C, and a Tree-sitter grammar for editor integration. An EBNF grammar and canonical test suite give all of them a shared contract. If your target language isn't on the list, the EBNF is the spec you implement against.
Output formats. CookCLI converts .cook files to JSON, YAML, Markdown, HTML, LaTeX, and Schema.org without any code on your part:
The JSON output preserves the inline structure — each step is a list of typed tokens (text, ingredient, cookware, timer) rather than a flat string, so you can render ingredient references highlighted in context:
This token-level representation is what flat-string formats can never give you.
Ecosystem. Active development. CookCLI, mobile apps (iOS and Android), LSP for editor integration, a federation protocol for sharing recipe collections, and a playground at playground.cooklang.org for testing syntax in-browser.
Best for. Apps where you need both human authoring and machine-structured data. Recipe storage, scaling, shopping list generation, editor tooling.
JSON-LD / Schema.org
Ingredient representation. This is the critical weakness: recipeIngredient is an array of plain strings. "3 cloves garlic, minced" is not structured data — it's a human-readable sentence. If you want the quantity (3), unit (cloves), ingredient name (garlic), and preparation (minced) as separate fields, you have to NLP your way to them. Schema.org defines no standard sub-structure for ingredient quantities.
Parser availability. Any JSON parser works. The format is JSON with known keys. If you're reading recipe data from the web (food blogs, recipe sites, Google's structured data), you'll encounter this constantly.
What it's actually for. JSON-LD Recipe markup is an SEO format, not a storage format. Nobody writes it by hand — CMSs and site generators produce it as a <script type="application/ld+json"> block embedded in HTML. The consumer is Google, not your code.
The round-trip problem. If you use JSON-LD as your source of truth, you can't reliably reconstruct structured ingredients from recipeIngredient strings without external parsing. The format is lossy in that direction. The practical pattern is to store recipes in a structured format (Cooklang, a database schema) and generate JSON-LD as an output:
Best for. SEO. Generate it from your source format; don't store in it.
MealMaster
Ingredient representation. Ingredients are in a fixed-width columnar block before the instructions. The column layout is:
Quantities occupy columns 1-7, unit columns 9-10, ingredient name from column 12 onward. That fixed layout is what makes parsing fragile — any tool that re-wraps lines, changes encoding, or alters spacing silently breaks the format.
Parser availability. A handful of converters exist — mostly unmaintained Perl and Python scripts from the early 2000s. There is no canonical parser, no grammar specification, and no test suite. Implementations disagree on edge cases. Column offsets vary slightly between versions of the original MealMaster software. If you're writing a MealMaster parser, expect to handle multiple dialects and write extensive test cases from real-world files.
Why it matters. The BBS recipe archives from the 1990s contain hundreds of thousands of recipes in this format. If you're building an import tool for a recipe manager, you will encounter .mmf files. The format is read-only for most practical purposes — nobody is authoring new recipes in MealMaster.
Best for. Import pipelines for legacy archives. Convert to anything else as soon as possible.
RecipeML
Ingredient representation. This is the most structurally complete of the four. Each ingredient is a tree with separate nodes for quantity, unit, item, and preparation:
Quantity, unit, name, and prep are unambiguously separated. No string parsing required. XML validation catches malformed documents. On paper, this is good format design.
Parser availability. Any XML parser works for the basic structure. The problem is the ecosystem is dead. RecipeML 0.5 was the last release, published in 2002. There is no active development, no community, no tooling beyond what was built in the early 2000s. The domain formatdata.com where the spec was hosted no longer resolves. You can find the spec mirrored in various places, but no one is maintaining implementations.
The verbosity problem. The XML sample above uses 47 lines to represent 5 ingredients and 4 steps. The same recipe in Cooklang is 8 lines. That verbosity was tolerable in 2002 when XML was considered the future of everything. It is not tolerable now.
Best for. Maintaining legacy systems that already use it. If you're starting fresh, the ingredient structure RecipeML provides (separate qty/unit/name/prep fields) is exactly what Cooklang's parser gives you from inline annotations — without the XML tax.
Comparison Table
| Feature | Cooklang | JSON-LD | MealMaster | RecipeML |
|---|---|---|---|---|
| Human-readable | Yes | No | Somewhat | No |
| Structured ingredients | Yes (inline) | No (strings) | Yes (columns) | Yes (XML nodes) |
| Parser ecosystem | 15+ languages | Every JSON parser | Few, unmaintained | Dead |
| Active development | Yes | Google-backed | No | No |
| Inline markup | Yes | N/A | No | No |
| Formal grammar / spec | Yes (EBNF + test suite) | Yes (Schema.org) | No | Yes (XSD, archived) |
| Best for | Authoring + tooling | SEO output | Legacy import | Legacy maintenance |
When to Use What
Building a recipe app from scratch. Use Cooklang as your source format. You get human-editable plain text files, a structured JSON representation from the parser, and a path to generate Schema.org output for any recipes you publish on the web. The 15+ parser implementations mean you're not betting on a single library.
Publishing recipes on a website that needs Google rich snippets. Generate JSON-LD from whatever you store internally. cook recipe --format schema does this if your source is Cooklang. If your source is a database, generate the JSON-LD at render time from your schema. Do not use JSON-LD as your canonical storage format.
Importing an existing recipe archive. If the archive is MealMaster, you're writing a conversion script. Parse the fixed-width columns, extract qty/unit/name, output to whatever format you actually want to work with. Do it once, carefully, and move on.
Maintaining an existing codebase that uses RecipeML. Keep it running. If you have the opportunity to migrate, Cooklang's ingredient structure (separate name, quantity, unit, and notes) maps cleanly to RecipeML's <item>, <qty>, <unit>, and <prep> fields. The migration is mechanical.
Need to understand the format before writing code. The Cooklang spec has the EBNF grammar and the canonical test suite. The playground lets you experiment in-browser. For Schema.org, the Recipe type reference is the authoritative source. For MealMaster, you'll need to read archived documentation and cross-reference with real .mmf files — there is no clean spec.
What the Parser Returns
If you're evaluating formats for a new project, the most useful comparison is what the parser hands you when parsing is done. Here's what you can reliably extract from each format:
| Field | Cooklang | JSON-LD | MealMaster | RecipeML |
|---|---|---|---|---|
| Ingredient name | Yes | String parsing required | Yes (with correct columns) | Yes |
| Ingredient quantity | Yes | String parsing required | Yes (with correct columns) | Yes |
| Ingredient unit | Yes | String parsing required | Yes (two-char abbreviation) | Yes |
| Prep note | Yes (parenthetical) | Embedded in string | After semicolon | Yes (<prep>) |
| Cookware references | Yes | No | No | No |
| Timer values | Yes | No | No | No |
| Step-level ingredient refs | Yes | No | No | No |
| Metadata (title, yield) | Yes (YAML frontmatter) | Yes | Yes (fixed fields) | Yes |
The step-level ingredient reference column is the meaningful differentiator. Only Cooklang knows that the olive oil in step one is the same entity as the olive oil entry in the ingredient list — because they're the same token in the source text, not two separate data points that you have to correlate by string matching.
The Cooklang spec is the starting point if you want to understand the grammar before writing a parser or integrating the library. The for-developers page has the full list of parser implementations by language. If you want to see the parser output interactively, the playground shows the JSON AST in real time as you edit.
-Alex