Forecast JSON (schema v5)¶
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 = 5
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 | 5. 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¶
"ready" means every served method was chosen by a promoted release matching the
current dataset, config, and code fingerprints.
"degraded" means it was not, and the system fell back to equal_weight. 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`"
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. 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). The precipitation sum is linear and is coherent with the hourly path by construction.
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 |
precip_intensity_mmh is an intensity (mm/hour), not an accumulation — the
only place the units differ from the hourly block.
methods here names a minutely path construction (minutely_interp,
minutely_persistence, minutely_anchor_tau_1h, …) rather than a blender. See
Methods: combination §8.
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.