You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The drinks JSON is generated by a structured backend, and the app's fromJson parsing defends against many type variants. An empirical survey of all data the API currently serves shows most of that variance never actually occurs — the data is far cleaner than the parsing (and the current loose schema) implies. This issue captures that finding, proposes a descriptive-strict JSON Schema that matches reality, and identifies the defensive code a schema + validation gate would let us delete.
This is learning-capture + tech-debt, not a user-facing bug. No behaviour changes proposed yet.
Empirical survey (method)
Fetched live (all HTTP 200) on 2026-05-28 and measured the runtime type of every variant-prone field:
only 6: Some beer remaining(246), Plenty left(160), Sold Out(121), A little remaining(107), ''(98), Nearly finished!(20)
bounded set
Winter festival (cbfw2025) corroboration — and one important nuance
The winter drop confirms the thesis but proves the variant unions are per-festival, so you cannot infer the schema from any single festival:
Field
Summer (777)
Winter (123)
Takeaway
abv
String 100%
String 100%
clean everywhere
is_vegan / vegan
is_vegan bool/null/absent; vegan never
both entirely absent (123/123)
is_vegan is per-festival, not chronological (winter is Dec 2025 yet has none); vegan still appears nowhere
year_founded
str(205)/int(108)/null
str(64)/null(3), zero int
winter quotes all years → union is real
allergens values
int/bool/string
int(116)/string(56), zero bool
bool only in some festivals → full int/bool/string union is required
allergen string values
all '' (absent)
all '' (absent)
string-skip stays correct; no real allergens dropped (verified)
status_text
6 values
adds new value Arrived(4)
vocabulary is not stable across festivals — see note below
dispense
—
adds keykeg (cask/keg/keykeg)
enum must cover winter variants
Key conclusions
Most variance is mechanical, not chaotic.year_founded is just "quoted vs unquoted integer"; allergen values are spellings of present/absent (1/true = present, false/'' = absent). The string-skip in allergen parsing is correct — every string value across both summer and winter is ''.
abv, bar, is_vegan are single-typed in practice — the variant handling matches what the schema permits, not what the API emits.
The variant unions are per-festival. Winter is all-string year_founded with no bool allergens; summer is mixed. A strict schema must therefore keep both unions — it can't be narrowed from one festival's data.
The vegan "legacy" claim is unverifiable.vegan appears in zero served datasets (cbf2023, cbfw2025, cbf2024-26). The "legacy" label exists only in the schema doc and the is_vegan ?? vegan code comment — no observable data supports it. Pre-2023 data that might use it is 404'd.
Is a strict schema possible?
Yes — JSON Schema (additionalProperties: false, required, enum, pattern, type unions) is expressive enough. The constraint is the data: it can only be as strict as the backend is consistent. Two flavours:
Descriptive-strict (achievable now, no backend access): tighten to exactly what the API emits while keeping the two real unions. Proposed below.
Prescriptive-strict (needs backend normalization): define the ideal (abv: number, is_vegan: boolean, year_founded: integer, allergen values boolean), fix the data pipeline to conform, then validate. Out of scope here — the data pipeline is upstream.
Once a schema + CI validation gate guarantees the shapes, these branches in lib/models/drink.dart become dead and can be simplified:
abv (lines ~85-86): drop the is num branch → double.tryParse((json['abv'] as String?) ?? '') ?? 0.0
bar (lines ~119-120): drop the is int branch → json['bar'] as String?
is_vegan (lines ~124-139): drop the is num / is String branches and the ?? json['vegan'] fallback → json['is_vegan'] as bool?
Must keep (genuinely variant across festivals): year_founded int/String parsing (Producer.fromJson ~21-27) and the allergen int/bool/num handling (Product.fromJson ~96-110).
⚠️ Removing the vegan fallback is the one judgment call: it's absent from all currently served data (verified across cbf2023, cbfw2025, cbf2024-26) but we can't inspect pre-2023 snapshots. Safe to remove for the app, since the app only loads festivals from the live registry.
Suggested validation approach
Validating against live data in CI is flaky (data changes hourly during a festival). Instead:
Commit representative fixtures (summer + winter, one per category incl. low-no) under test/fixtures/ and validate them against the strict schema in a validate:drinks mise task, mirroring the existing validate:festivals pattern.
Optionally add a separate, non-blocking scheduled job that validates live data and reports drift.
Related
Availability status: replace fragile substring matching with an ordered status model #348 — availability substring matching. The winter data adds the status value Arrived (maps to plenty), not in the summer set — proving the status_text vocabulary is not stable across festivals. This is actually an argument for keeping resilient matching over a hard-coded enum, provided no new value produces a false substring match. Also note Some beer remaining (most common summer status, ~32%) currently maps to AvailabilityStatus.low via the remaining substring, which under-represents availability.
Files: docs/code/api/beer-list-schema.json, lib/models/drink.dart, mise.toml (new validate:drinks task), test/fixtures/ (new).
Summary
The drinks JSON is generated by a structured backend, and the app's
fromJsonparsing defends against many type variants. An empirical survey of all data the API currently serves shows most of that variance never actually occurs — the data is far cleaner than the parsing (and the current loose schema) implies. This issue captures that finding, proposes a descriptive-strict JSON Schema that matches reality, and identifies the defensive code a schema + validation gate would let us delete.This is learning-capture + tech-debt, not a user-facing bug. No behaviour changes proposed yet.
Empirical survey (method)
Fetched live (all HTTP 200) on 2026-05-28 and measured the runtime type of every variant-prone field:
beer, cbf2025beer/cider/mead, cbf2026beer→ 777 drinks / 356 producersbeer+low-no→ 123 drinks / 67 producers (full type census; first survey of thelow-nocategory)vegan-key probe additionally across cbf2023 (201 drinks)https://data.cambeerfestival.app/festivals.json) serves only:cbf2026,cbf2025,cbfw2025,cbf2024. cbf2022 and older → 404.What the data actually contains (summer census, 777 drinks)
fromJsonhandlesidnamecategorydispenseabvstylenotesstatus_textbarbarnever int/bool)is_veganveganveganare deadallergensvalues1(410) / booltrue(207)false(72) / string''(207)year_foundedstatus_text(distinct values)Some beer remaining(246),Plenty left(160),Sold Out(121),A little remaining(107),''(98),Nearly finished!(20)Winter festival (cbfw2025) corroboration — and one important nuance
The winter drop confirms the thesis but proves the variant unions are per-festival, so you cannot infer the schema from any single festival:
abvis_vegan/veganveganneveris_veganis per-festival, not chronological (winter is Dec 2025 yet has none);veganstill appears nowhereyear_foundedallergensvaluesallergenstring values''(absent)''(absent)status_textArrived(4)dispensekeykeg(cask/keg/keykeg)Key conclusions
year_foundedis just "quoted vs unquoted integer"; allergen values are spellings of present/absent (1/true= present,false/''= absent). The string-skip in allergen parsing is correct — every string value across both summer and winter is''.abv,bar,is_veganare single-typed in practice — the variant handling matches what the schema permits, not what the API emits.year_foundedwith no bool allergens; summer is mixed. A strict schema must therefore keep both unions — it can't be narrowed from one festival's data.vegan"legacy" claim is unverifiable.veganappears in zero served datasets (cbf2023, cbfw2025, cbf2024-26). The "legacy" label exists only in the schema doc and theis_vegan ?? vegancode comment — no observable data supports it. Pre-2023 data that might use it is 404'd.Is a strict schema possible?
Yes — JSON Schema (
additionalProperties: false,required,enum,pattern, type unions) is expressive enough. The constraint is the data: it can only be as strict as the backend is consistent. Two flavours:abv: number,is_vegan: boolean,year_founded: integer, allergen valuesboolean), fix the data pipeline to conform, then validate. Out of scope here — the data pipeline is upstream.Proposed descriptive-strict rules (replacing
docs/code/api/beer-list-schema.json)abv:type: string,pattern: ^\\d+(\\.\\d+)?$(NOTnumber)is_vegan:type: [boolean, null]; removeveganfrom the schemabarstylenotesstatus_text:type: [string, null]category,dispense: keepenum— ensuredispenseincludeskeykegadditionalProperties: falseyear_founded:type: [string, integer, null], stringpattern: ^\\d{4}$,minimum: 1000,maximum: 2100(union retained)allergens:additionalProperties: { type: [integer, boolean, string] }(union retained)Defensive code this would unlock for removal
Once a schema + CI validation gate guarantees the shapes, these branches in
lib/models/drink.dartbecome dead and can be simplified:abv(lines ~85-86): drop theis numbranch →double.tryParse((json['abv'] as String?) ?? '') ?? 0.0bar(lines ~119-120): drop theis intbranch →json['bar'] as String?is_vegan(lines ~124-139): drop theis num/is Stringbranches and the?? json['vegan']fallback →json['is_vegan'] as bool?Must keep (genuinely variant across festivals):
year_foundedint/String parsing (Producer.fromJson~21-27) and the allergen int/bool/num handling (Product.fromJson~96-110).Suggested validation approach
Validating against live data in CI is flaky (data changes hourly during a festival). Instead:
low-no) undertest/fixtures/and validate them against the strict schema in avalidate:drinksmise task, mirroring the existingvalidate:festivalspattern.Related
Arrived(maps toplenty), not in the summer set — proving thestatus_textvocabulary is not stable across festivals. This is actually an argument for keeping resilient matching over a hard-coded enum, provided no new value produces a false substring match. Also noteSome beer remaining(most common summer status, ~32%) currently maps toAvailabilityStatus.lowvia theremainingsubstring, which under-represents availability.Files:
docs/code/api/beer-list-schema.json,lib/models/drink.dart,mise.toml(newvalidate:drinkstask),test/fixtures/(new).