The recipe bodies survive. The structure around them does not. A 900-recipe Paprika export lands in Mealie with names, ingredient text, directions, source URLs and the primary photo intact, but the per-ingredient quantity, unit and food fields arrive as unparsed strings, and anything Paprika stores that has no equivalent in Mealie's recipe model goes quietly: the photo array beyond the first image, the scale factor, the difficulty label, the favourites flag and Paprika's free text nutrition block. Treat this as two separate jobs, the import itself, which is one file upload and a few minutes of waiting, and a scripted repair pass against the Mealie API, which is where the actual work sits.
TL;DR by profile:
- The Paprika 3 owner with 900 recipes and ten years of star ratings: import everything in one pass, then audit field by field before you cancel anything, because the loss is concentrated in metadata you cannot reconstruct from the recipe text.
- The Nextcloud Cookbook self-hoster moving sideways: expect the cleanest of the three migrations, since each recipe is already a schema.org JSON document that maps almost one to one onto Mealie's model.
- The RecipeKeeper user on Windows and iOS: budget the most repair time of the three, because the export is an HTML document plus an image folder and everything Mealie gets has to be scraped back out of markup.
- The solo developer with Python 3 and an API token: script the repair, because the whole job is a paginated GET over /api/recipes plus a PATCH per recipe, and 900 recipes is roughly 20 lines of code away from fixed.
- The household cook who will never open a terminal: keep the paid app installed and read only for one full season, because the shopping list and offline behaviour matter more than the field mapping.
- Anyone still evaluating: run the import against a throwaway Mealie instance with a separate database first, because the importers are not idempotent and a bad run is easier to delete than to unpick.
The tradeoff is plain: Mealie gives you a structured, queryable library you own, and you pay for that structure by rebuilding the parts of your Paprika data that were never structured to begin with.
Table of contents
- What a 900-recipe Paprika export actually contains
- Where each Paprika field lands inside Mealie
- Which Paprika fields disappear without a warning?
- Why do your ingredients arrive as plain text instead of structured quantities?
- Do your photos survive, and what happens to the second and third one?
- Ratings, favourites, scale factors and the metadata that never arrives
- How does the Nextcloud Cookbook importer compare with the Paprika one?
- What does a RecipeKeeper export lose on the way into Mealie?
- How do you audit the import before you cancel the subscription?
- Duplicates, failed recipes and the ones that silently never arrive
What a 900-recipe Paprika export actually contains
Export from Paprika 3 and you get a single file with a .paprikarecipes extension. Rename it to .zip and open it: inside sits one gzip-compressed file per recipe, each with a .paprikarecipe extension. Decompress one and you have a flat JSON object. That is the entire format. There is no schema file, no index, no relational structure between recipes.
Run unzip -l library.paprikarecipes | wc -l before you import anything. If your library holds 900 recipes, you should see 900 entries plus the zip header lines. This is your baseline count, and you will compare it against Mealie's recipe total afterwards.
Each JSON object carries a predictable set of keys:
-
Identity fields:
uidandhash, a UUID and a content hash that Paprika uses for sync, with no destination in Mealie's model. -
Free text blocks:
ingredients,directions,notesanddescription, all single strings with newline separators rather than arrays. -
Timing and yield:
prep_time,cook_time,total_timeandservings, all stored as human strings such as1 hr 20 min, not as integers or ISO 8601 durations. -
Image payloads:
photo_dataholding a base64 JPEG, plusphoto,photo_hash,image_urland aphotosarray for additional images. -
Classification and personal signal:
categoriesas an array of strings, plusrating,difficulty,on_favorites,scale,source,source_url,createdand a free textnutritional_info.
Nothing here is invalid. It is simply a sync format for one application, not an interchange format, and that distinction explains every loss described below.
Where each Paprika field lands inside Mealie
The import itself runs inside the instance, from the Migrations page under group data management. You upload the .paprikarecipes file through the browser, so the whole library crosses your reverse proxy in one request. A 900-recipe export with embedded base64 photos is comfortably large enough to hit the nginx default client_max_body_size of 1m, which returns a 413 before Mealie ever sees the file. Raise that limit first, whether Mealie runs on a self-managed VPS, a home server, a NAS or a Yundera box. Yundera is a managed Personal Cloud Server, built on CasaOS, that runs self-hosted apps as Docker containers on a server dedicated to the user.
| Paprika field | Mealie destination | What changes on the way |
|---|---|---|
name |
name plus generated slug
|
Duplicate names get a numeric suffix appended to the slug |
ingredients |
recipeIngredient array |
Split on newlines into one entry per line, stored as original text |
directions |
recipeInstructions array |
Split into steps, with empty step titles and no grouping |
categories |
Categories and tags | Created on demand, so 40 Paprika categories become 40 new records |
photo_data |
data/recipes/<id>/images/ |
Base64 JPEG decoded and rewritten as original, min and tiny WebP |
source_url |
orgURL |
Preserved verbatim, including dead links |
prep_time, cook_time
|
prepTime, performTime
|
Kept as the original human strings, not parsed into minutes |
Every row above is a field that arrives. The next section covers the keys with no row at all.
Which Paprika fields disappear without a warning?
Nothing in the interface tells you what did not make it. The import reports a count of recipes created and stops there. These are the keys that have no destination in Mealie's recipe model:
-
difficulty: Paprika stores an easy, medium or hard label per recipe, and Mealie has no equivalent field anywhere in its schema, so the value is read and discarded. -
scale: the saved serving multiplier vanishes. Mealie scales at view time fromrecipeYield, which means a recipe you always cooked at 2x reverts to the base quantities in the text. -
on_favorites: favourites in Mealie live on the user record, not the recipe record, so a flag set on 120 recipes leaves you with an empty favourites list for every account. -
nutritional_info: Paprika holds one free text blob. Mealie expects discrete numeric fields for calories, fat, protein, carbohydrate, fibre, sodium and sugar, and a blob cannot be split into seven typed columns without parsing. -
uidandhash: the sync identifiers are replaced by fresh Mealie UUIDs, which removes any stable key for matching a recipe back to its source file later.
Count before you mourn. Decompress the export and run something like for f in *.paprikarecipe; do gunzip -c "$f"; done | jq -r 'select(.difficulty != "") | .name' | wc -l against each field in turn. Most libraries of 900 recipes have a difficulty set on a few dozen and a scale set on fewer still. Repair effort belongs where the field is actually populated, not where it merely exists in the format.
Why do your ingredients arrive as plain text instead of structured quantities?
Because the two applications model an ingredient differently, and the importer refuses to guess.
-
Mealie stores five fields per ingredient: a numeric
quantity, aunitreference, afoodreference, a freenoteand the untouchedoriginalText. Units and foods are separate database tables with their own records, not strings on the recipe. -
Paprika stores one line of text:
2 cups all-purpose flour, siftedis a single string with no delimiters, and the export has no field telling you which part is the amount. Mealie's importer keeps the whole line inoriginalTextand leaves quantity, unit and food unset rather than inventing a split. -
Scaling stops working: the serving multiplier in the Mealie interface multiplies the numeric
quantity. With no quantity stored, pressing 2x changes the yield label and leaves every ingredient line reading2 cupsexactly as before. - Shopping list aggregation stops working: Mealie merges list entries that point at the same food record. Import three recipes needing onions and you get three separate unmergeable lines, because there is no onion record to match on.
- Food and unit filters stay empty: browsing by food, or filtering the recipe list on a specific ingredient, queries those tables. After a raw import of 900 recipes, both tables can still contain zero rows.
The volume matters here. At an average of 10 ingredient lines per recipe, 900 recipes is around 9,000 strings needing structure. That is the single largest repair job in the migration, and Mealie ships a parser for exactly this, covered further down.
Do your photos survive, and what happens to the second and third one?
One photo per recipe survives. The rest do not.
-
The primary image is decoded and re-encoded: the base64 JPEG in
photo_datais written to disk as WebP in three sizes,original.webp,min-original.webpandtiny-original.webp, underdata/recipes/<uuid>/images/. The picture is intact, but the bytes are not, so checksum comparison against the source file will never match. -
The
photosarray has no destination: Paprika lets you attach several images to one recipe. The Mealie recipe model carries a single main image plus a separate assets folder, and the migration does not populate assets, so photo two onwards is read and dropped. -
Recipes holding only
image_urlarrive blank: if a recipe has a remote URL and no embedded payload, nothing is fetched during the import and the card shows the default placeholder image. -
Image storage leaves the database: everything lands on the filesystem, not in SQLite or PostgreSQL. A
pg_dumpalone is not a backup of your library, anddu -sh data/recipesafter import is the number that matters for your disk planning.
Count the damage before you decide whether to care. Decompress the export and run jq 'select((.photos | length) > 1) | .name' across the files. Most libraries keep multi-photo recipes in the low dozens, usually the ones you cooked and photographed yourself. If the count is under 50, re-attaching them by hand through the recipe editor is faster than writing an uploader.
Ratings, favourites, scale factors and the metadata that never arrives
Some of this is recoverable from the export file. Some of it is gone for good, because it was never in the export to begin with.
| Signal you had | Where it would live in Mealie | What you can do about it |
|---|---|---|
| Star ratings | Recipe rating, and a separate per-user rating in recent releases |
Verify one known 5-star recipe after import, then PATCH the rest from the JSON if the view is empty |
| Favourites flag | A per-user favourites relation, not a recipe column | Rebuild with one API call per recipe, filtered on on_favorites in the source files |
| Saved scale factor | No field exists | Fold the multiplier into recipeYield text by hand, only for recipes you always cooked scaled |
| Date added |
createdAt and dateAdded
|
All 900 recipes share the import date, so sort by name until you backfill from the created key |
| Last cooked date and history |
lastMade plus timeline events |
Nothing arrives, and nothing can, because Paprika never exported a cooking log |
| Tools and equipment | A dedicated tools table | Starts empty, since Paprika has no equivalent concept to map from |
Sort your repair list by whether the data exists somewhere. Ratings, favourites and creation dates all sit in the decompressed .paprikarecipe files, so they are a scripted backfill and nothing more. The cooking timeline is different. Mealie builds it from events you record after the fact, which means the history starts the day you import and your previous years of cooking simply do not exist in the new system. Accept that one rather than hunting for a workaround.
How does the Nextcloud Cookbook importer compare with the Paprika one?
It is the cleaner of the two, because the source format was designed for interchange rather than sync.
-
The unit of migration is a folder, not a blob: Nextcloud Cookbook stores each recipe as a directory holding
recipe.jsonplusfull.jpgandthumb.jpg. Zip the parent directory before uploading, and confirm the count first withfind . -name recipe.json | wc -l. -
recipe.jsonis a schema.org Recipe document: the same vocabulary Mealie uses internally and the same vocabulary its URL scraper targets, so the mapping is field to field rather than key to guess. -
Ingredients arrive pre-split:
recipeIngredientis already a JSON array with one entry per ingredient, which removes every newline splitting error. The strings themselves are still unstructured, so the parsing job from the previous section is unchanged and unavoidable. -
Durations are ISO 8601: a value of
PT1H20Mlands as a real duration instead of the human text strings that come across from a Paprika export. - Nutrition maps key by key: the schema.org nutrition object exposes calories, fat, protein and carbohydrate as separate properties, which drop straight into Mealie's seven nutrition fields with no parsing.
-
Keywords become tags directly:
keywordsis a flat list, so there is no ambiguity about whether a value belongs in categories or tags.
The loss is different in kind. Nextcloud Cookbook never stored a favourites flag, a cooking log or a personal star rating, so there is no personal signal to lose and nothing to backfill. You migrate structure, and your history was already absent.
What does a RecipeKeeper export lose on the way into Mealie?
RecipeKeeper hands over the weakest of the three formats, because it exports a web page rather than a data file.
-
The whole library is one document: a zip containing
recipes.htmlplus an images directory. There is no per-recipe file, so your baseline count comes from markup, not from a file listing:grep -c 'itemprop="name"' recipes.html. -
Parsing depends on microdata attributes surviving intact: the importer reads
itempropvalues out of the HTML. Open and resave that file in an editor that rewrites markup and the import will find fewer recipes, report success, and give you no error to investigate. - Photos are referenced by relative path, not embedded: the images folder has to stay inside the zip at the exact path the HTML expects. Rearrange the archive and recipes import cleanly with no picture and no warning.
-
Unicode fractions come through literally: RecipeKeeper writes ½ and ¼ as single characters. They survive into
originalTextfine, but the ingredient parser handles1/2 cupfar more reliably than the glyph, which shifts work into the repair pass. - Personal fields are presentation markup: course, rating and source are styled page elements rather than typed columns, so what arrives depends entirely on the markup shape of your export version.
- No stable identifiers exist anywhere: run the import twice and you get two complete copies of every recipe, with no key available to match and deduplicate them afterwards.
Verify a sample of 10 recipes chosen to cover every field you care about before you trust a run of 900.
How do you audit the import before you cancel the subscription?
Create an API token from your user profile, then check the library from the outside rather than by scrolling it.
-
Compare totals before anything else: run
curl -H "Authorization: Bearer $TOKEN" "https://your-mealie-host/api/recipes?page=1&perPage=1" | jq .totaland hold it against the file count you took from the export. If 900 went in and 894 came out, you have six files to find. -
Diff the name lists, not just the counts: pull every name with
perPage=1000, sort it, and diff against the names extracted from the source. Numeric slug suffixes expose duplicate titles, and absent names expose skipped files, which counts alone will hide when a duplicate offsets a failure. - Sample 30 recipes at random rather than the first 30: the top of an alphabetical list tends to hold your oldest and simplest entries. Random sampling surfaces the recipes with tables in the directions, nested lists or multiple photos.
- Probe images with HEAD requests, not with clicks: loop over the recipe IDs and request the image path for each. Counting 404 responses takes one loop and tells you exactly how many cards are showing a placeholder.
- Count the fields you expect to be empty: query how many ingredient entries have no food reference and how many recipes have a null rating. Those two numbers define the size of the repair work ahead.
-
Archive the export file permanently: the
.paprikarecipeszip is now the only copy of every field Mealie discarded, so store it beside your backups.
A matching count proves the rows moved. It does not prove the library is usable.
Duplicates, failed recipes and the ones that silently never arrive
A migration that half worked looks identical to one that worked. The interface reports what it created, never what it skipped.
- Individual files fail for boring reasons: a character encoding problem, a truncated base64 photo payload or a malformed JSON body stops that one recipe. The run continues, the total comes back lower than your baseline, and nothing in the browser says which file died.
-
The container log is the only witness: watch it during the run rather than afterwards, with something like
docker logs -f mealiein a second terminal. Per-file errors are named there and nowhere else. - Duplicate titles are usually genuine recipes: Paprika happily stores two entries called Roast Chicken. Mealie keeps both and appends a numeric suffix to the second slug. Deleting everything with a suffixed slug is a fast way to lose real data, so compare bodies before you prune.
- The genuine duplicate risk is re-running the import: the migration creates records, it does not match and update them. Upload the same file twice and 900 recipes become 1,800, with no key available to pair the copies back together.
-
Tag the run so it has a boundary: if the migration form offers to tag what it creates, enable it. Otherwise apply a dated tag such as
import-2026-04immediately afterwards through bulk actions, so one filtered/api/recipes/bulk-actions/deletecall unwinds the whole attempt. - Take a Mealie backup before every attempt: restoring a backup from the settings area is a single operation, while deleting 900 recipes by hand is an evening you will not enjoy.
Top comments (0)