The CSV Boundary Is Where Ingredient Trees Lose Their Meaning
Fresh Open Food Facts export changes show how root ingredients, leaf ingredients, nested JSON, reference-food mappings, and snapshot metadata should coexist without double-counting or hiding analytical grain.
Two export changes reveal three different ingredient lists
On August 25, Open Food Facts changed a Foodture export so its “top ingredients by quantity” calculation includes only leaf ingredients. It also added a separate set of the first ten root ingredients, preserved the complete ingredient and packaging structures as JSON, and added CIQUAL reference-food data to an ingredient summary. Four days earlier, another change had added five years of country and global scan counts and switched the extraction job from cached public API responses to direct product storage so those scan records were available.
The changes shipped together in the Open Food Facts v2.103.0 release on August 26. They look like additions to one CSV, but they expose a general problem for recipe, nutrition, grocery, and food-data APIs: there is no context-free answer to “what are this product’s ingredients?”
A label-order view needs root ingredients. An additive analysis needs terminal ingredients after compounds are expanded. An audit or future recalculation needs the whole tree. A representative-product dataset also needs to say when, how, and from which storage surface each row was selected. Putting all of these into a wide file without declaring their grain can produce clean-looking, mathematically invalid analyses.
The thesis: bulk food-data exports should provide explicitly named root, leaf, and full-tree projections plus a snapshot manifest, because hierarchy level and extraction boundary are part of every quantity, ranking, and reference-food join.
Source map and the repeated angle to avoid
Fresh primary evidence from the last seven days:
| Source | Date | What it contributes |
|---|---|---|
| Open Food Facts Foodture extract columns change | 2026-08-21 | Adds annual country/global scan fields and changes extraction from cached API responses to direct product and scan storage. |
| Open Food Facts ingredient export change | 2026-08-25 | Separates leaf-only quantity rankings from root ingredients, adds complete ingredient and packaging JSON, adds CIQUAL fields and a proxy flag, and changes CSV parsing to handle embedded newlines. |
| Open Food Facts v2.103.0 release | 2026-08-26 | Confirms both export changes shipped in the same server release. |
Recent posts here have already covered preserving parser evidence, traversing ingredient hierarchies for environmental methods, and treating external food IDs as purpose-bound crosswalks. The repeated angle to avoid is “trees and provenance matter.” The new issue is the analytical boundary: how should an API turn one nested record into stable bulk projections without inviting a buyer, notebook, warehouse, or model-training pipeline to sum incompatible nodes?
“Top ten ingredients” is not a complete specification
Consider a simplified pesto whose percentages are all expressed on the same final-product basis:
pesto
├── basil 40%
├── oil blend 30%
│ ├── olive oil 20%
│ └── sunflower oil 10%
├── cheese 20%
└── salt 10%
If an export collects every node and sorts by quantity, the apparent total is 130 percent: oil blend is counted alongside the two children that already compose it. Nothing in the resulting numbers necessarily looks broken. The file parses, every value is numeric, and a top-ten query returns rows. The error appears later as inflated ingredient prevalence, distorted nutrition allocation, or a training target that rewards compound labels and their contents simultaneously.
Leaf-only aggregation returns basil, olive oil, sunflower oil, cheese, and salt. That is suitable for questions such as “how much sunflower oil appears after compounds are expanded?” Root-only extraction returns basil, oil blend, cheese, and salt in label order. That is suitable for reconstructing the declared top-level composition or displaying the package hierarchy. Neither replaces the other.
The situation is harder when child percentages are relative to their parent. If olive oil is recorded as two-thirds of the oil blend rather than 20 percent of the final product, an exporter must carry the basis or calculate the path product before comparing it with basil. Sorting raw percentages from mixed levels is invalid even if only leaves are selected.
Publish projections, not one flattened truth
A useful export contract distinguishes at least three projections:
| Projection | Node set | Order | Appropriate uses | Unsafe use |
|---|---|---|---|---|
root_ingredients |
Direct children of the product or recipe | Declared source order | Label display, first-ingredient logic, source reconstruction | Expanded allergen or commodity coverage |
leaf_ingredients |
Terminal nodes after compound expansion | Quantity rank or stable path order, explicitly stated | Additive aggregation, nutrition candidates, commodity analysis | Reconstructing how the label grouped ingredients |
ingredient_tree |
Every node with parent-child edges | Stable traversal order | Audit, reprocessing, alternate projections, scope-sensitive rules | Blind summation across hierarchy levels |
The Open Food Facts change follows this general pattern by placing root columns beside leaf-only top-quantity columns and complete JSON. For an API provider, a normalized bulk shape is usually easier to evolve than repeatedly adding numbered columns:
{
"productId": "product:example-pesto",
"projection": "leaf_ingredients",
"projectionVersion": "2026-08-26",
"quantityBasis": "percent_of_final_product",
"ranking": {
"metric": "quantity_estimate",
"limit": 10,
"tieBreaker": "stable_node_path"
},
"ingredients": [
{
"nodeId": "node:olive-oil",
"ingredientId": "ingredient:olive-oil",
"path": ["node:oil-blend", "node:olive-oil"],
"quantity": 20,
"unit": "percent",
"quantityStatus": "estimated"
}
]
}
A CSV can represent the same view, but its sidecar schema must define projection, quantity basis, ordering, truncation, null behavior, and the relationship to the full tree. Column names alone cannot carry all of that safely.
The storage boundary belongs in the export manifest
The earlier Foodture change moved extraction from cached API responses to direct product folders, partly to avoid rate limits and partly to retrieve scan data. That is a reasonable operational choice for an internal job, but it changes what reproducibility means.
An API response may apply field selection, permissions, normalization, or a documented version contract. Direct storage may expose fields unavailable through the public API and may observe product data and scan data at slightly different moments. Removing a cache also changes whether rerunning the job uses the old retrieved representation or today’s current record.
The export therefore needs a manifest, not only a generation timestamp:
{
"exportId": "food-export-2026-08-26",
"generatedAt": "2026-08-26T18:00:00Z",
"sourceBoundary": "internal_product_and_scan_storage",
"sourceRevision": "v2.103.0",
"selectionPolicy": "representative_product_by_country_category",
"selectionPolicyVersion": "2026-08-26",
"productSnapshotAt": "2026-08-26T17:30:00Z",
"scanWindow": { "firstYear": 2021, "lastYear": 2025 },
"projections": ["root_ingredients", "leaf_ingredients", "ingredient_tree"],
"referenceMappings": [{ "system": "CIQUAL", "relationField": "is_proxy" }]
}
This separates three dates that teams often collapse: when source facts were observed, which time window a metric summarizes, and when the file was generated. A 2025 scan count inside a 2026 export is not a 2026 observation. A current ingredient tree joined to historical popularity metrics is a mixed-time record unless the contract says otherwise.
Edge cases that break otherwise valid files
Repeated leaves. The same canonical ingredient can occur under two compounds. Decide whether the leaf view contains occurrences or aggregates by ingredient ID. If aggregated, preserve contributing node paths and quantity bases.
Unknown quantities. Missing quantity is not zero. Top-N rankings should state whether unknowns are excluded, placed after measured values, or ranked by declared order. Coverage should report how much known mass the selected leaves represent.
Reference-food proxies. The fresh export adds CIQUAL codes and an explicit proxy indicator. Keep that relation beside the mapping. A proxy can support an estimate but should not be presented as an exact measured food match.
Numbered-column truncation. “First ten roots,” “top ten leaves,” and “first five packaging components” are lossy projections. Always include total counts and truncation flags, or preserve a complete nested field or companion table.
Embedded JSON and newlines. Once complete trees are placed in a CSV cell, quoting and row framing become correctness requirements. The August 25 change also switched to record-oriented CSV parsing to handle newlines. Consumers should use a standards-aware parser, never split records on commas or physical lines.
Unmatched representative rows. A country/category target with no product match is different from a matched product with missing ingredients. Give selection failures a status and reason instead of emitting an ambiguous mostly blank row.
Contract checklist for export producers and buyers
Before publishing or accepting a recipe or food-data export, verify:
- Is the row grain documented separately from the ingredient-node grain?
- Are root order, leaf ranking, and complete hierarchy available as distinct views?
- Can parent and child quantities ever be summed in the same metric?
- Does every percentage state whether it is relative to the parent or final product?
- Are repeated ingredient occurrences distinguishable from canonical-ID aggregates?
- Are unknown, estimated, declared, calculated, and zero quantities separate states?
- Do top-N and numbered columns include total counts, coverage, tie rules, and truncation flags?
- Are exact and proxy nutrition-reference mappings distinguishable?
- Does a manifest identify source boundary, software revision, snapshot time, metric windows, and selection policy?
- Can a consumer reproduce the projection from the complete tree?
- Are embedded JSON, quotes, commas, and newlines covered by round-trip fixtures?
- Can operators diff two exports by source change, projection change, or reference-mapping change?
These questions apply to recipe data as directly as packaged products. A recipe can contain sub-recipes, optional toppings, divided quantities, and prepared components. Grocery exports may need root lines for shopping intent while nutrition calculations need expanded leaves. Model-training datasets need to know whether “oil blend” and “olive oil” are two examples or one hierarchy.
The CSV boundary should simplify transport, not erase meaning. The durable pattern is to export several declared projections from one recoverable tree, identify the snapshot that produced them, and make lossy choices visible. That gives Recipe API builders and technical buyers data they can aggregate, join, audit, and retrain on without discovering months later that a perfectly valid file counted the same ingredient twice.
Sources
- Open Food Facts, feat: Add more columns to the Foodture extract, merged August 21, 2026.
- Open Food Facts, feat: Add more ingredient information to the Foodture export, merged August 25, 2026.
- Open Food Facts, v2.103.0 release notes, published August 26, 2026.
Start Building
One consistent schema on every response. Get a free key and ship in minutes.