Forecast JSON (schema v6)¶
The document predict emits. This is the system's integration surface — if you
are consuming forecasts from another program, this page is the contract.
Implemented in: serve/schema.py, SCHEMA_VERSION = 6
grounded-weather-forecast predict --out forecast.json
Loading it back into typed objects:
from grounded_weather_forecast.serve.schema import Forecast
forecast = Forecast.from_json(open("forecast.json").read())
print(forecast.status, len(forecast.hourly))
Design principle: every number carries its provenance¶
Most forecast APIs give you a number. This document gives you a number plus how it was produced, which evidence justified it, what it is measuring, and how confident the system is that it should be serving that method at all.
That is why each point carries parallel dictionaries keyed by variable name
rather than a flat list of values. A consumer that only wants numbers reads
values and ignores the rest; a consumer that needs to defend a decision has
everything it needs.
Top level¶
| Field | Type | Meaning |
|---|---|---|
schema_version |
int | 6. Check it. |
issued_at |
ISO 8601 | the information boundary — nothing after this instant informed the document |
latitude, longitude |
float | station location |
timezone |
IANA name | defines date_local in the daily block |
dataset_fingerprint |
str | identity of the matrices this forecast was fitted on |
sources |
list of str | the provider/model slugs available at this snapshot |
observation_at |
ISO 8601 or null |
timestamp of the station reading used for anchoring; null means no usable observation |
status |
"ready" | "degraded" |
see below |
status_reason |
str or null |
why, when degraded |
release_ids |
list of str | every promoted release that contributed |
minutely |
list | next 60 minutes |
hourly |
list | next 48 hours |
daily |
list | next 10 local days |
status¶
With automatic selection, "ready" means a finite hourly or daily value carries a
promoted release compatible with current code, config, product sources, features,
and truth semantics. A retained selection keeps its original evaluation dataset;
the document dataset fingerprint identifies the inputs actually used. Check each variable's
release_ids for its own attribution. A minutely promotion alone does not
change document readiness.
Hourly and daily values are normalized once before attribution: null, NaN, or
infinite model outputs have no release ID and cannot justify readiness. Zero is
a finite value and retains valid attribution.
Explicit --method runs keep their existing "ready" status and can have
empty release attribution.
"degraded" means no hourly or daily value retains a promoted release. The
system may use equal_weight or rainfall reconciled from its child forecasts. The
forecast is still usable — an ungrounded equal-weight blend is a reasonable
forecast — but it is not the evidence-backed one. Typical reasons:
"status": "degraded",
"status_reason": "implementation changed since the last backtest; re-run `backtest --source live` then `report`"
Rainfall safeguards can also remove the last served release despite compatible
promotion evidence. Prediction-local effects record actual attribution loss
from finite served fields; method labels alone do not determine the reason. If
both safeguards remove attribution, the wet-day reason takes precedence over
reconciliation. Without either loss, cold-start, synthetic-only, dataset/config,
and implementation-change diagnoses retain their existing wording. Detailed wet-day
counts remain in each affected field's selection_reasons. These effects are not
serialized; the JSON schema remains v6.
Cold-start blends have no release or guard-loss attribution even when provider
values are finite. A null rainfall value later filled from native minute evidence
also records no attribution loss. Archived documents replay as originally served;
these emission rules do not rewrite historical forecasts.
An internal fitting diagnostic records when a release-bearing selection cannot
be fitted from scoreable training history, including choices displaced by a rain
guard. It does not count as attribution loss. After checking archive provenance
and dataset/config/code identities, compatible evidence receives the specific
fitting reason, scoped to product, variable, and truth semantics. The prefix
“some compatible promoted selections could not be fitted at issuance” permits
mixed outcomes. The fitter reports its actual unavailable branch. Causes
distinguish a missing/empty matrix, missing provider forecast columns, no finite
training truth, and the two issuance cutoffs. Pending truth on already-issued
rows calls for polling as live issuance advances. Evidence from later-issued
rows was unavailable at the requested issuance. Repeating a fixed historical
--now cannot admit future evidence in either case; rebuilding alone is not
required. Unknown history receives inspection advice. Other cases say no promoted release produced a finite hourly or daily
forecast. Per-field reasons retain their selection history. Diagnostics retain
only small counts and failure records and are not serialized.
Implementation-change diagnosis takes precedence when code, configuration and
dataset identities have changed together, preserving the automatic restore trigger.
Approved minutely selections alone do not establish hourly/daily readiness: the
reason requests backtest --source live rather than only polling. The approved-load
marker remains distinct from arbitrary library-supplied selections. Serving derives
truth and causal observation features from one observation read per snapshot.
Within a product snapshot, later successful or unpromoted attempts do not erase earlier promoted-fit failures. A surviving finite attributed forecast still keeps the document ready; these records do not independently demote readiness.
Degraded status is also printed to stderr, so --out - still yields clean
JSON on stdout. See the FAQ.
Degraded is a feature
The alternative — serving a promoted method whose justifying evidence no longer applies — is how a system quietly starts lying. See ADR 0005.
hourly[]¶
| Field | Type | Meaning |
|---|---|---|
valid_time |
ISO 8601 | the hour this point is about |
lead_hours |
float | valid_time − issued_at, in hours |
lead_bucket |
str or null |
the bucket that governed method selection |
values |
{variable: float\|null} |
the forecast |
methods |
{variable: method_id} |
which blender produced each value |
quantiles |
{variable: {level: value}} |
predictive quantiles, when available |
quantiles_source |
{variable: str} |
where the quantiles came from |
selection_reasons |
{variable: str} |
why that method was chosen |
release_ids |
{variable: release_id} |
which promotion justified it |
truth_semantics |
{variable: "inst"\|"mean"} |
what the number is measuring |
A real point:
{
"valid_time": "2026-08-08T18:00:00+00:00",
"lead_hours": 3.25,
"lead_bucket": "3-6h",
"values": {
"temp_c": 29.807,
"humidity_pct": 26.104,
"dew_point_c": 8.847,
"wind_speed_ms": 3.268,
"wind_gust_ms": 5.844,
"pressure_sea_hpa": 1013.585,
"precip_mm": 0.0,
"pop": 0.03
},
"methods": { "temp_c": "equal_weight", "...": "..." },
"quantiles": {},
"quantiles_source": {},
"selection_reasons": { "temp_c": "no backtest evidence for this slice", "...": "..." },
"release_ids": {},
"truth_semantics": { "temp_c": "mean", "wind_gust_ms": "inst", "...": "..." }
}
Reading truth_semantics¶
This is the field most consumers overlook and the one most likely to cause a subtle integration bug.
"inst"— the value is the instantaneous level atvalid_time."mean"— the value is the interval mean over[valid_time, +1h).
In the example above temp_c is an interval mean while wind_gust_ms is
instantaneous, because a gust is an extreme and a mean gust is not a
meaningful object. If you compare these numbers against your own observations,
compare like with like — mismatching the convention manufactures roughly 1 °C of
apparent error on a clear day. See
Methods: truth and QC §2.
Reading quantiles and quantiles_source¶
quantiles is empty when the serving method is a point forecaster and no
dressing pool was available. A variable with a null normalized point has neither
quantiles nor a source label. Empty distributions are omitted, and source labels
require usable emitted quantiles. When present, keys are string levels:
"quantiles": {
"temp_c": { "0.05": 26.1, "0.25": 28.4, "0.75": 31.2, "0.95": 33.8 }
}
quantiles_source records provenance. An absent key means the quantiles came
from the method itself (EMOS, IDR, conformal, analog); a present value names
the residual-dressing or recalibration path that produced them. This matters
because a method-native distribution and a dressed one carry different
guarantees — see Methods: calibration.
Quantiles are always monotone in level (rearranged) and coherent across related
variables (temp_min ≤ temp_max, dew_point ≤ temp) at every level, not just
at the median.
Reading selection_reasons¶
Human-readable strings explaining the choice. "no backtest evidence for this
slice" is the cold-start case; others name the gate that promoted or blocked a
method. Paired with release_ids, a scored row can be attributed to the exact
promotion that served it.
daily[]¶
Same shape as hourly[], keyed by local day:
| Field | Type | Meaning |
|---|---|---|
date_local |
YYYY-MM-DD |
the local calendar day, per timezone |
lead_days |
int | days ahead |
values, methods, quantiles, quantiles_source, selection_reasons, release_ids, truth_semantics |
as above |
Typical variables: temp_max_c, temp_min_c, pop, precip_sum_mm.
Daily extremes are their own supervised targets, not aggregates of the hourly
path — so a daily max need not equal the max of the hourly block, and that is
intentional (ADR 0002).
Automatic daily rainfall equals the hourly sum for a complete future local day.
For partial days it is at least the known hourly accumulation, plus today's
observed rain and the native minute prefix before the first future hour. Its method and
reason distinguish an exact sum from a lower bound. Daily PoP is at least the
largest contained native minute or hourly PoP; measurable rain already observed today sets its
occurrence to 100%. See Rainfall coherence.
Changed reconciled fields clear inherited uncertainty and release attribution;
unchanged values within absolute tolerance 1e-12 retain all original metadata.
minutely[]¶
A flatter shape, because the minutely product serves a fixed variable set.
| Field | Type | Meaning |
|---|---|---|
valid_time |
ISO 8601 | the minute |
minutes_ahead |
int | 0–60 |
temp_c, humidity_pct, dew_point_c, wind_speed_ms, precip_intensity_mmh, pop |
float or null |
the forecast |
methods |
{variable: method_id} |
the promoted path construction, per variable |
quantiles |
{variable: {level: value}} |
when available |
release_ids |
{variable: release_id} |
the release selecting the method actually used |
selection_reasons |
{variable: str} |
the selected method's reason, including blocked live replacements |
truth_semantics |
{variable: "inst"} |
truth target for the attributed selection |
precip_intensity_mmh is an intensity (mm/hour), not an accumulation — the
only place the units differ from the hourly block. Divide by 60 for the one-minute
interval ending at valid_time. Latest native paths are interpolated inside
their support across gaps of at most 15 minutes. Open-Meteo's preceding
15-minute accumulations instead use constant rates over each reported interval
to conserve their amounts. Its hourly rain fields are aligned to interval
starts during archive reading. Outside native coverage, an
available matching hour supplies a named hourly_rainfall_fallback: its unused
rainfall amount is shared uniformly across missing minute slots, including
unshown slots, while its PoP is copied. Native minute values are preserved.
Missing parent evidence stays null; future hours are not borrowed into earlier
gaps. The fallback is an assumed shape and cannot raise its own hourly floor.
methods here names a minutely path construction (minutely_interp,
minutely_persistence, minutely_anchor_tau_1h, …) rather than a blender. See
Methods: combination §8.
The provenance dictionaries are optional in older documents and default to empty. A selected minutely method receives release attribution only when it actually runs. Already-anchored hourly paths, missing-observation substitution, failed fitted responses, and native precipitation paths do not inherit an unused minutely selection's release. The top-level release list includes all attributed hourly, daily, and minutely values. Historical minutely rows without provenance remain unattributed; schema 6 does not reconstruct their release.
Compatibility¶
Forecast.from_json accepts documents that omit optional blocks (minutely,
hourly, daily, timezone, status, status_reason, release_ids), which is
what lets schema-1 documents still load. Required in every version:
schema_version, issued_at, latitude, longitude, dataset_fingerprint,
sources.
Serialization uses allow_nan=False: a missing value is JSON null, never
NaN. The document is therefore valid JSON for every strict parser.
Check schema_version before parsing. A version bump means fields changed.
Approval metadata compatibility¶
Release content is immutable and new release objects require committed approval
events. Current managed approvals follow committed sequence across clock
corrections. Historical queries require publication, approval and the conservative
visible_at floor within their cutoff. Missing visible_at fields derive floors
in sequence order without rewrites. Malformed, naive and overflowing clocks remain
invalid. Legacy JSON without approval_required retains
implicit approval only with an acceptable aware publication timestamp. This does
not change forecast schema 6 or active_release.json attribution. Context-free
legacy selections remain limited to their recorded dataset. Per-slice
legacy_context_missing markers preserve that restriction alongside modern
contexts sharing an evaluation ID; historical "unknown" IDs mean absent
provenance. Known evaluation IDs are retained for evidence protection.
Gate measurements live in diagnostic history
and do not rotate release identity when the incumbent remains approved.