Technical / Part 4
Part 4 — Data Model & Schema
The canonical JSON record of one as-ordered drink: what every field means, the machine-validatable schema, and the serialization rules that make documents hashable.
dbnf-1.schema.json
4.1The DBNF document
One document = one as-ordered drink. It carries enough to re-derive the label (base + modifiers), plus the engine's output (computed, declared, warnings) so consumers of the document don't need an engine.
{
"dbnf": "1.0-draft",
"drink": {
"name": "Caramel Latte",
"vendorSku": "lat-car-16",
"servingSize": { "amount": 16, "unit": "fl_oz", "ml": 473 },
"servingsPerContainer": 1
},
"base": {
"calories": 250, "totalFat_g": 7, "satFat_g": 4.5, "transFat_g": 0,
"cholesterol_mg": 25, "sodium_mg": 170, "totalCarb_g": 35,
"fiber_g": 0, "sugars_g": 33, "addedSugars_g": 28,
"protein_g": 10, "caffeine_mg": 150
},
"modifiers": [
{ "type": "milk_substitution", "from": "reduced_2pct", "to": "oat_barista", "milkVolume_ml": 250 },
{ "type": "sweetness", "factor": 1.0, "method": "total_sugars_approx" },
{ "type": "syrup_pump", "count": 3, "syrup": "vanilla", "sugars_g": 5, "calories": 20 },
{ "type": "espresso_shot", "count": 2, "caffeine_mg": 75 }
],
"computed": {
"calories": 320, "totalFat_g": 7.25, "satFat_g": 2.0, "transFat_g": 0,
"cholesterol_mg": 25, "sodium_mg": 170, "totalCarb_g": 54.5,
"fiber_g": 0, "sugars_g": 43, "addedSugars_g": 43,
"protein_g": 5, "caffeine_mg": 300,
"dv": { "totalFat_pct": 9, "satFat_pct": 10, "cholesterol_pct": 8,
"sodium_pct": 7, "totalCarb_pct": 20, "fiber_pct": 0,
"addedSugars_pct": 86 },
"sizeMethod": "published_recipe",
"sweetnessMethod": "total_sugars_approx"
},
"declared": {
"calories": "320", "totalFat": "7g", "satFat": "2g", "transFat": "0g",
"cholesterol": "25mg", "sodium": "170mg", "totalCarb": "55g",
"fiber": "0g", "sugars": "43g", "addedSugars": "43g",
"protein": "5g", "caffeine": "300mg"
},
"warnings": ["added_sugars_over_50pct_dv", "caffeine_over_half_daily"],
"cup": {
"id": "A92F-3",
"deposit": { "amount": 1.00, "currency": "USD" },
"state": "in_use", "cycles": 14
},
"meta": {
"generatedAt": "…", // RFC 3339 UTC
"engine": "bevfacts-ref/1.0",
"vendor": { "name": "Example Coffee", "locationId": "loc_042" },
"labelId": "lbl_9f2ke1"
}
}
4.2Entity reference
| Field | Type | Req. | Semantics |
|---|---|---|---|
dbnf | string | MUST | Spec version the document was computed under (§4.6) |
drink.name | string ≤ 80 | MUST | As-ordered display name |
drink.vendorSku | string | MAY | Vendor's recipe identifier — enables audit against the recipe DB |
drink.servingSize | object | MUST | unit ∈ {fl_oz, ml}; ml always present regardless of display unit |
base | NutrientVector | MUST | Unmodified recipe, unrounded, all 12 components present (zeros explicit) |
modifiers[] | Modifier[] | MUST | In pipeline order (Part 2 §2.2); empty array = as-published drink |
computed | NutrientVector + dv + methods | MUST | Engine output, unrounded; method flags record estimation fallbacks |
declared | object of strings | SHOULD | Part 3 §3.5 grammar, print-ready |
warnings[] | enum[] | SHOULD | Closed set from Part 3 §3.6 + Part 2 §2.7 |
cup | Cup | MAY | Part 6; omit entirely for disposable service |
meta.generatedAt | RFC 3339 | MUST | UTC, second precision |
meta.engine | string | SHOULD | name/version of the computing engine |
4.3Modifier types (closed union)
Unknown type values MUST be rejected (422), not skipped — silently dropping a modifier understates the drink.
| type | Required fields | Optional fields |
|---|---|---|
size | ml_ordered | ml_base (else from servingSize) |
ice_level | level ∈ {none, light, regular, extra} | factor (vendor-measured override) |
milk_substitution | from, to, milkVolume_ml | fromVector, toVector (per-100 mL overrides) |
sweetness | factor ∈ [0, 1.5] | method |
syrup_pump | count ∈ [0, 12] | syrup, sugars_g, calories, full vector |
espresso_shot | count ∈ [0, 8] | caffeine_mg, decaf (bool) |
topping | id | vector (else vendor topping table) |
4.4JSON Schema (2020-12)
The machine-validatable schema. Abbreviated only where noted; the pattern for every nutrient key is identical.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bevfacts.com/technical/dbnf-1.schema.json",
"title": "DBNF Document",
"type": "object",
"required": ["dbnf", "drink", "base", "modifiers", "computed", "meta"],
"properties": {
"dbnf": { "type": "string", "pattern": "^1\\.0(-draft(\\.\\d+)?)?$" },
"drink": {
"type": "object",
"required": ["name", "servingSize"],
"properties": {
"name": { "type": "string", "maxLength": 80 },
"vendorSku": { "type": "string" },
"servingSize": {
"type": "object",
"required": ["amount", "unit", "ml"],
"properties": {
"amount": { "type": "number", "exclusiveMinimum": 0 },
"unit": { "enum": ["fl_oz", "ml"] },
"ml": { "type": "number", "exclusiveMinimum": 0 }
}
},
"servingsPerContainer": { "type": "integer", "minimum": 1, "default": 1 }
}
},
"base": { "$ref": "#/$defs/nutrientVector" },
"modifiers": {
"type": "array",
"items": {
"type": "object",
"required": ["type"],
"oneOf": [
{ "properties": { "type": { "const": "size" },
"ml_ordered": { "type": "number", "exclusiveMinimum": 0 } },
"required": ["type", "ml_ordered"] },
{ "properties": { "type": { "const": "ice_level" },
"level": { "enum": ["none", "light", "regular", "extra"] } },
"required": ["type", "level"] },
{ "properties": { "type": { "const": "milk_substitution" },
"from": { "type": "string" }, "to": { "type": "string" },
"milkVolume_ml": { "type": "number", "minimum": 0 } },
"required": ["type", "from", "to", "milkVolume_ml"] },
{ "properties": { "type": { "const": "sweetness" },
"factor": { "type": "number", "minimum": 0, "maximum": 1.5 } },
"required": ["type", "factor"] },
{ "properties": { "type": { "const": "syrup_pump" },
"count": { "type": "integer", "minimum": 0, "maximum": 12 } },
"required": ["type", "count"] },
{ "properties": { "type": { "const": "espresso_shot" },
"count": { "type": "integer", "minimum": 0, "maximum": 8 } },
"required": ["type", "count"] },
{ "properties": { "type": { "const": "topping" },
"id": { "type": "string" } },
"required": ["type", "id"] }
]
}
},
"computed": { "$ref": "#/$defs/nutrientVector" },
"declared": { "type": "object", "additionalProperties": { "type": "string" } },
"warnings": {
"type": "array",
"items": { "enum": [
"added_sugars_over_50pct_dv", "added_sugars_over_100pct_dv",
"caffeine_over_half_daily", "caffeine_over_daily",
"clamped_negative", "energy_reconciliation_failed" ] }
},
"cup": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string", "pattern": "^[0-9A-F]{4,8}(-[0-9A-F])?$" },
"deposit": {
"type": "object", "required": ["amount", "currency"],
"properties": {
"amount": { "type": "number", "minimum": 0 },
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" }
}
},
"state": { "enum": ["issued", "in_use", "returned", "washing", "clean", "lost", "retired"] },
"cycles": { "type": "integer", "minimum": 0 }
}
},
"meta": {
"type": "object", "required": ["generatedAt"],
"properties": {
"generatedAt": { "type": "string", "format": "date-time" },
"engine": { "type": "string" },
"labelId": { "type": "string" }
}
}
},
"$defs": {
"nutrientVector": {
"type": "object",
"required": ["calories", "totalFat_g", "satFat_g", "transFat_g",
"cholesterol_mg", "sodium_mg", "totalCarb_g", "fiber_g",
"sugars_g", "addedSugars_g", "protein_g", "caffeine_mg"],
"properties": {
"calories": { "type": "number", "minimum": 0 },
"totalFat_g": { "type": "number", "minimum": 0 },
"satFat_g": { "type": "number", "minimum": 0 },
"transFat_g": { "type": "number", "minimum": 0 },
"cholesterol_mg": { "type": "number", "minimum": 0 },
"sodium_mg": { "type": "number", "minimum": 0 },
"totalCarb_g": { "type": "number", "minimum": 0 },
"fiber_g": { "type": "number", "minimum": 0 },
"sugars_g": { "type": "number", "minimum": 0 },
"addedSugars_g": { "type": "number", "minimum": 0 },
"protein_g": { "type": "number", "minimum": 0 },
"caffeine_mg": { "type": "number", "minimum": 0 }
}
}
}
}
Cross-field invariants (added ≤ sugars ≤ carbs, energy reconciliation) exceed JSON Schema's expressiveness — they are enforced by the engine (Part 2 §2.7) and re-checked at the API boundary (Part 5 §9.1, error nutrient_conflict).
4.5Canonical serialization
For hashing, signing, and deduplication, a canonical byte form is defined (profile of RFC 8785 JCS):
- UTF-8, no BOM; object keys sorted lexicographically at every level;
- no insignificant whitespace;
- numbers in shortest round-trip form (
7.25, never7.250); noNaN/Infinity— an engine that produces them is broken upstream; - timestamps in UTC with
Zsuffix, second precision; - the document hash is
sha256(canonical_bytes), rendered lowercase hex — used as the dedup key and the optional print-audit reference.
4.6Document versioning
The dbnf field pins a document to the spec version it was computed under. Readers MUST accept unknown optional fields (forward compatibility) and MUST reject documents whose major version they don't implement. A document is immutable once issued: recomputing (e.g. a recipe correction) produces a new document with a new generatedAt and hash — the old label on the old cup stays honest about what was known when it printed.