Skip to content

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 at valid_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.