Ingredient Roles Deserve Their Own Annotation Layer
Recent Open Food Facts changes to additive-class propagation and NOVA-related ingredient translations show why recipe and nutrition APIs should attach roles, processing signals, and evidence to ingredient entities instead of encoding them as pseudo-ingredients.
The thesis
Ingredient parsers should separate what the ingredient is from what role it plays in the label, recipe, or nutrition model. When a system encodes roles such as colour, preservative, mechanically separated, hydrolysed, smoked, concentrated, or powdered as ordinary ingredient nodes, it creates avoidable errors in search, nutrition estimation, allergen review, grocery matching, and AI explanations.
That distinction matters because food-data systems are moving beyond simple ingredient strings. In the last week, Open Food Facts merged a change that makes additive classes properties of parsed additives rather than separate ingredient entries. The example in the pull request turns Colours: carmine, e101, tartrazine, sugar into ingredient entities for en:e120, en:e101, and en:e102, each annotated with additive_class: en:colour, instead of a tree where en:colour appears as a parent ingredient. Open Food Facts also merged new translations for ingredient entries carrying nova:en properties, including mechanically separated meat, hydrolysed cereal, modified flour, whey product, concentrated whey protein, fruit juice concentrate, and soy preparation.
Those two changes point to the same API design lesson: ingredient identity, ingredient role, processing state, nutrition classification, and label evidence are different layers. Treating them as one field may be convenient during ingestion, but it makes downstream products brittle.
The repeated angle this avoids
Recent posts on this blog have already covered localized taxonomies, label grammar, taxonomy review workflows, API client versioning, and generic metadata-extension contracts. This article is narrower: it focuses on the annotation layer around an ingredient entity. The question is not just how to translate labels or govern taxonomy updates. It is how recipe, nutrition, grocery, and food-AI APIs should represent contextual signals that modify an ingredient without changing its identity.
That is a practical modeling decision, not a taxonomy housekeeping detail.
Why pseudo-ingredients break product behavior
A pseudo-ingredient is any parsed node that looks like an ingredient but is really a role, class, process, heading, or labeling convention. Examples include:
colourinColours: carmine, tartrazinepreservativeinPreservatives: sodium nitritesmokedinsmoked paprikamechanically separatedinmechanically separated chickenconcentratedinfruit juice concentratepowderedinmilk powderhydrolysedinhydrolysed cereal
Some of these can be canonical ingredient identities in specific contexts. Others are modifiers. Many can be either, depending on language, product category, and the label grammar used by the manufacturer. That ambiguity is exactly why the API contract needs to expose structure instead of flattening everything into a list of strings.
If colour is returned as an ingredient, a recipe search feature may think the product contains a food called colour. If modified is folded into the ingredient name with no normalized processing signal, a nutrition scoring feature may be unable to explain why two flour ingredients are treated differently. If hydrolysed cereal is only a translated display string, an allergen workflow may miss that the source grain still matters. If concentrated whey protein is normalized too aggressively to whey, a sports-nutrition app may lose the product distinction its users actually care about.
The core failure mode is not one bad parse. It is a data model that cannot express the difference between identity and context.
A better contract: ingredient plus annotations
A durable API response can keep a stable ingredient entity while attaching typed annotations that describe how the ingredient appeared and how the system interpreted it.
{
"id": "ing_01",
"canonicalIngredientId": "en:e102",
"displayName": "tartrazine",
"sourceText": "Colours: carmine, e101, tartrazine, sugar",
"position": {
"start": 24,
"end": 34
},
"annotations": [
{
"type": "additive_class",
"valueId": "en:colour",
"label": "colour",
"scope": "ingredient",
"evidence": "label_heading",
"confidence": 0.98
}
],
"normalization": {
"matchedBy": "additive_code_or_synonym",
"taxonomyVersion": "2026-07-28",
"language": "en"
}
}
The important design choice is that en:colour is not pretending to be another ingredient. It is a role annotation attached to the ingredient entities that inherited that role from the label heading.
The same pattern can represent processing and nutrition-classification signals:
{
"canonicalIngredientId": "en:mechanically-separated-chicken",
"displayName": "mechanically separated chicken",
"annotations": [
{
"type": "processing_state",
"valueId": "en:mechanically-separated",
"scope": "ingredient",
"evidence": "ingredient_phrase",
"confidence": 0.94
},
{
"type": "nutrition_classification_signal",
"scheme": "nova",
"valueId": "en:mechanically-separated-meat",
"scope": "ingredient",
"evidence": "taxonomy_property",
"confidence": 0.9
}
]
}
This does not require every API to expose the same fields. But it does require product teams to stop hiding these distinctions inside untyped strings.
Product decisions that become easier
| Product question | If roles are pseudo-ingredients | If roles are annotations |
|---|---|---|
| Search for recipes without artificial colours | Query may match colour as a fake ingredient or miss additives inherited from a heading |
Filter ingredients by annotations.type=additive_class and valueId=en:colour |
| Explain nutrition or processing warnings | Hard to know which token caused the warning | Link the warning to evidence, taxonomy version, and ingredient scope |
| Match packaged foods to grocery items | Role words can pollute ingredient similarity and product embeddings | Use identity fields for matching and annotations for feature flags |
| Localize ingredient labels | Translations may overwrite analytical meaning | Keep canonical IDs stable while localizing display names and annotation labels |
| Audit AI extraction | Model output is a flat list with unclear provenance | Compare predicted annotations against parser evidence and taxonomy rules |
This separation also helps technical buyers evaluate recipe and food-data APIs. A vendor that returns only ingredients: ["colour", "tartrazine", "sugar"] may be fine for display. It is not enough for compliance-adjacent search, allergen workflows, nutrition reasoning, or grocery substitutions.
Edge cases to design for
Label headings that apply to multiple following ingredients
The Open Food Facts additive-class example is a common pattern: a heading such as Colours: applies to several additives. Your parser should preserve the heading as evidence and propagate the role to the relevant child ingredients. Do not rely on the heading's position alone after normalization; once ingredients are reordered, deduplicated, or grouped, the evidence link disappears unless it is explicit.
Phrases where the modifier is part of the identity
fruit juice concentrate is not always best modeled as fruit juice plus a generic concentrated flag. For grocery and nutrition products, concentrate may be central to identity, category, or nutrition expectations. The API can still expose a processing annotation, but the canonical ingredient ID should be chosen carefully. A good rule is: if removing the modifier changes the purchasable or nutritional concept, keep it in identity and also expose the modifier as a queryable signal.
Language-specific morphology
Recent Swedish ingredient-processing additions show why role extraction cannot be English-only. Gender, plural forms, compound words, and label grammar can change where the modifier appears. The model should store canonical IDs separately from localized labels, and parser releases should be versioned so clients can understand why results changed.
Additive codes and names
A user may write E102, e102, tartrazine, or a localized additive name. The identity layer should normalize those aliases to the additive entity. The role layer should then describe whether the additive was declared under a class such as colour, preservative, antioxidant, acidity regulator, or sweetener. Do not infer the role solely from the additive's possible uses when the label provides stronger evidence.
AI-generated or AI-extracted recipes
Food-AI systems are especially prone to collapsing identity and role because natural-language explanations sound plausible. If an LLM extracts smoked as an ingredient or silently drops hydrolysed, the result may still read well while damaging filters and nutrition logic. Treat AI output as candidate annotations with confidence and evidence, not as final normalized facts.
Implementation checklist
Before exposing advanced ingredient filters or nutrition explanations, API teams should be able to answer these questions:
- Do ingredient entities have stable canonical IDs independent of display text?
- Can one ingredient carry multiple annotations, such as additive class, processing state, preparation state, and nutrition-classification signal?
- Does each annotation include scope: ingredient, ingredient group, whole product, recipe step, or label section?
- Is there evidence for the annotation, such as source text span, label heading, taxonomy property, model prediction, or human review?
- Can clients filter by annotation type without treating the annotation value as an ingredient?
- Are taxonomy and parser versions exposed so clients can debug changed classifications?
- Can the API represent uncertainty and conflicting evidence?
- Are localized labels separate from canonical IDs and analytics keys?
- Does the search index distinguish identity tokens from role tokens?
- Can rejected or ambiguous annotations be reviewed without rewriting the ingredient list?
If the answer to most of these is no, a single ingredients[] array is doing too much work.
Operational trade-offs
Annotation layers add complexity. They require more schema design, more parser tests, and more documentation. They may also force clients to decide whether to show raw label text, normalized ingredient names, or interpreted warnings.
But the alternative is usually worse. Once roles are mixed into ingredients, every downstream service invents its own cleanup logic. Search removes some terms. Nutrition scoring adds special cases. Grocery matching ignores tokens. AI explanations hallucinate reasons. Analytics teams count modifiers as foods. Eventually, the platform has many interpretations of the same label and no authoritative contract.
A good migration path is incremental:
- Keep the existing ingredient list for backward compatibility.
- Add optional
annotationsto each ingredient entity. - Expose parser and taxonomy versions.
- Add filters over annotation types, not only ingredient IDs.
- Deprecate pseudo-ingredient behavior only after clients can migrate.
This is a natural place for Recipe API-style infrastructure to add value: not by claiming every food label is perfectly understood, but by giving developers a structured contract for the uncertainty and context around each parsed ingredient.
The practical takeaway
Ingredient normalization is no longer just string cleanup. Modern recipe, nutrition, grocery, and food-AI products need to know whether a token is an ingredient, a role, a processing state, a classification signal, or a piece of evidence. Recent Open Food Facts work on additive-class propagation and NOVA-related translations is a useful reminder that these distinctions should live in the API shape, not in scattered application code.
For builders, the design principle is simple: keep ingredient identity stable, attach contextual roles as typed annotations, and preserve the evidence that produced them. That model makes search more precise, nutrition explanations more auditable, grocery matching cleaner, and AI extraction safer to operate.
Sources
- Open Food Facts pull request, feat: Propagate additive classes to parsed additive ingredients, merged July 28, 2026.
- Open Food Facts pull request, taxonomy: Add translations for ingredients with nova:en property, merged July 28, 2026.
- Open Food Facts pull request, taxonomy: Update ingredients_processing taxonomy, merged July 28, 2026.
- Open Food Facts pull request, taxonomy(ingredient_processing): Add more Swedish entries, add gender variant/plural, merged July 28, 2026.
Start Building
One consistent schema on every response. Get a free key and ship in minutes.