BeerJSON-first recipes (canonical storage)
Status: Implemented (2026-02-17)
Scope: Backend (API + DB) and web recipe editor are BeerJSON-first.
Goals
- Use BeerJSON as the canonical representation of recipe ingredients for import/export interoperability.
- Keep app-specific inputs and overrides in
recipeExtJson(versioned), without polluting canonical BeerJSON.
Canonical storage (Postgres + Prisma)
recipes.beer_json_recipe_json: canonical BeerJSON document.recipes.recipe_ext_json: internal extensions (versioned).
Legacy recipe JSON columns were removed:
recipes.grist_jsonrecipes.hops_jsonrecipes.yeast_jsonrecipes.misc_json
API contract (recipes)
Recipe create/update is BeerJSON-first:
POST /recipes- Requires:
name,styleKey,beerJsonRecipeJson - Optional:
notes,recipeExtJson
- Requires:
PATCH /recipes/:id- Supports updates to:
name,notes,styleKey,beerJsonRecipeJson,recipeExtJson
- Supports updates to:
Name/notes synchronization rule
The DB columns recipes.name and recipes.notes are treated as the human-facing summary fields.
On create/update, the API normalizes BeerJSON so:
beerjson.recipes[0].namematchesrecipes.namebeerjson.recipes[0].notesmatchesrecipes.notes(or is removed if notes are empty)
recipeExtJson v1
Validated by AJV in the API.
Canonical “v1 real” fields (not defaults)
batchSizeLiters?: number(must be > 0)brewhouseEfficiencyPercent?: number(0..100)- Optional targets:
ogTarget?: { sg: number }fgTarget?: { sg: number }abvTarget?: { percent: number }
Internal mapping helpers
BeerJSON does not carry our internal linking/override metadata, so recipeExtJson also supports:
ingredientLinks: best-effort mapping from UI row IDs → canonical ingredient IDsgrist,hops,yeast,misc: objects mapping{ [rowId: string]: ingredientId }
mashPhModel: per-grist-row mash pH model overrides keyed by row IDmashDiPh,mashTaToPh57_mEqPerKg,roastDehuskedOverride
Row IDs inside BeerJSON (pragmatic extension)
To keep stable editor row identity across edits, the web editor includes an id field on BeerJSON addition objects:
ingredients.fermentable_additions[*].idingredients.hop_additions[*].idingredients.culture_additions[*].idingredients.miscellaneous_additions[*].id
This id is not part of the BeerJSON standard, but it is accepted by the upstream JSON-Schema (these addition types do not set additionalProperties: false).
Portability note: exported BeerJSON uses a strict mode that strips these internal row id fields.
Import/export behavior
- Import routes validate BeerJSON and map it into canonical storage:
- Single recipe:
POST /recipes/import/preview,POST /recipes/import - Bulk (multi-recipe files):
POST /recipes/import/bulk/preview,POST /recipes/import/bulk
- Single recipe:
- Export (strict BeerJSON) is implemented:
- Single recipe:
GET /recipes/:id/export/beerjson - Bulk export (all recipes in active account):
GET /recipes/export/beerjson - Strict export strips internal addition row
idfields for interoperability.
- Single recipe:
Files (source of truth)
- API BeerJSON validation:
services/api/src/beerjson/beerjsonValidator.ts - API recipe ext validation:
services/api/src/beerjson/recipeExtValidator.ts - API recipe persistence:
services/api/src/services/recipesService.ts - Web BeerJSON/editor mapping:
apps/web/app/recipes/_lib/beerjsonRecipe.ts