Skip to content

Commit 3e0a484

Browse files
committed
Generate jsonschema (draft/2020-12) from jsonformat structs. Update README and version schema in this repo.
1 parent 88f1619 commit 3e0a484

7 files changed

Lines changed: 240 additions & 15 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,22 @@ The code for the batch jobs that produce the datasets are not included in this r
1414

1515
[Getting started guide](docs/getting-started.md)
1616

17+
## Documentation
18+
19+
### Forti JSON format
20+
21+
Forti serializes the forecast timeseries in a custom json format, based on [GeoJSON](https://geojson.org/)
22+
23+
The format is semantically versioned with a [jsonschema](forti.schema.v1.0.0.json) in this repo.
24+
1725
## Development
1826

27+
### Release new json format version
28+
29+
```
30+
go run ./jsonfrontend/cmd/generate-jsonschema -version vX.Y.Z > forti.schema.json
31+
```
32+
1933
### Test
2034

2135
## Architecture

forti.schema.v1.0.0.json

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://raw.githubusercontent.com/metno/forti/forecast.schema.v1.0.0.json",
4+
"$ref": "#/$defs/GeoJSON",
5+
"$defs": {
6+
"Forecast": {
7+
"properties": {
8+
"meta": {
9+
"$ref": "#/$defs/Metadata"
10+
},
11+
"timeseries": {
12+
"items": {
13+
"$ref": "#/$defs/TimeStep"
14+
},
15+
"type": "array"
16+
}
17+
},
18+
"additionalProperties": false,
19+
"type": "object",
20+
"required": [
21+
"meta",
22+
"timeseries"
23+
]
24+
},
25+
"ForecastDetails": {
26+
"additionalProperties": {
27+
"type": "number"
28+
},
29+
"type": "object"
30+
},
31+
"GeoJSON": {
32+
"properties": {
33+
"type": {
34+
"type": "string",
35+
"enum": [
36+
"Feature"
37+
]
38+
},
39+
"geometry": {
40+
"$ref": "#/$defs/Geometry"
41+
},
42+
"properties": {
43+
"$ref": "#/$defs/Forecast"
44+
}
45+
},
46+
"additionalProperties": false,
47+
"type": "object",
48+
"required": [
49+
"type",
50+
"geometry"
51+
]
52+
},
53+
"Geometry": {
54+
"properties": {
55+
"type": {
56+
"type": "string",
57+
"enum": [
58+
"Point"
59+
]
60+
},
61+
"coordinates": {
62+
"items": {
63+
"type": "number"
64+
},
65+
"type": "array",
66+
"maxItems": 3,
67+
"minItems": 2,
68+
"description": "Longitude and latitude"
69+
}
70+
},
71+
"additionalProperties": false,
72+
"type": "object",
73+
"required": [
74+
"type",
75+
"coordinates"
76+
]
77+
},
78+
"Metadata": {
79+
"properties": {
80+
"updated_at": {
81+
"type": "string",
82+
"format": "date-time",
83+
"description": "Time of the last forecast update"
84+
},
85+
"error": {
86+
"type": "string",
87+
"description": "Error message if the forecast is unavailable"
88+
},
89+
"units": {
90+
"additionalProperties": {
91+
"type": "string"
92+
},
93+
"type": "object",
94+
"description": "Maps each forecast parameter name to its physical unit"
95+
},
96+
"radar_coverage": {
97+
"type": "string",
98+
"description": "Custom metadata used only for timeseries forecasts from radar data. Radar coverage status for the location"
99+
}
100+
},
101+
"additionalProperties": false,
102+
"type": "object",
103+
"required": [
104+
"updated_at",
105+
"units"
106+
]
107+
},
108+
"Summary": {
109+
"properties": {
110+
"symbol_code": {
111+
"type": "string",
112+
"description": "Weather symbol identifier"
113+
},
114+
"symbol_confidence": {
115+
"type": "string",
116+
"description": "Confidence level of the weather symbol"
117+
}
118+
},
119+
"additionalProperties": false,
120+
"type": "object",
121+
"required": [
122+
"symbol_code"
123+
]
124+
},
125+
"TimeStep": {
126+
"properties": {
127+
"time": {
128+
"type": "string",
129+
"format": "date-time",
130+
"description": "Start time of the forecast interval"
131+
},
132+
"data": {
133+
"additionalProperties": {
134+
"$ref": "#/$defs/TimestepData"
135+
},
136+
"type": "object",
137+
"description": "Forecast data keyed by interval length e.g. instance"
138+
}
139+
},
140+
"additionalProperties": false,
141+
"type": "object",
142+
"required": [
143+
"time",
144+
"data"
145+
]
146+
},
147+
"TimestepData": {
148+
"properties": {
149+
"summary": {
150+
"$ref": "#/$defs/Summary",
151+
"description": "Summary of the forecast parameters"
152+
},
153+
"details": {
154+
"$ref": "#/$defs/ForecastDetails",
155+
"description": "List of forecast parameters and their numerical values"
156+
}
157+
},
158+
"additionalProperties": false,
159+
"type": "object",
160+
"required": [
161+
"details"
162+
]
163+
}
164+
}
165+
}

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ require (
4040
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect
4141
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect
4242
github.com/aws/smithy-go v1.26.0 // indirect
43+
github.com/bahlo/generic-list-go v0.2.0 // indirect
4344
github.com/beorn7/perks v1.0.1 // indirect
45+
github.com/buger/jsonparser v1.1.2 // indirect
4446
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4547
github.com/go-logr/logr v1.4.3 // indirect
4648
github.com/go-logr/stdr v1.2.2 // indirect
@@ -50,8 +52,10 @@ require (
5052
github.com/google/wire v0.7.0 // indirect
5153
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
5254
github.com/googleapis/gax-go/v2 v2.19.0 // indirect
55+
github.com/invopop/jsonschema v0.14.0 // indirect
5356
github.com/kylelemons/godebug v1.1.0 // indirect
5457
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
58+
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
5559
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
5660
github.com/prometheus/client_model v0.6.2 // indirect
5761
github.com/prometheus/common v0.67.3 // indirect
@@ -63,6 +67,7 @@ require (
6367
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
6468
go.opentelemetry.io/otel/trace v1.43.0 // indirect
6569
go.yaml.in/yaml/v2 v2.4.3 // indirect
70+
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
6671
golang.org/x/crypto v0.51.0 // indirect
6772
golang.org/x/net v0.55.0 // indirect
6873
golang.org/x/oauth2 v0.36.0 // indirect

go.sum

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,12 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3
7474
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8=
7575
github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s=
7676
github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
77+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
78+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
7779
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
7880
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
81+
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
82+
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
7983
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
8084
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
8185
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
@@ -120,6 +124,8 @@ github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTs
120124
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
121125
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
122126
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
127+
github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg=
128+
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
123129
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
124130
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
125131
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -130,12 +136,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
130136
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
131137
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
132138
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
133-
github.com/metno/go-weathersymbol v0.1.1 h1:IgYHGkTFL28i0mlBo9b5BkF19+a3/pLxwz+tW7JwyOI=
134-
github.com/metno/go-weathersymbol v0.1.1/go.mod h1:YisSf88s5ZtgpZuKW4LgEB6IanDuCBt3lXfh4JB/diw=
135139
github.com/metno/forti-internalformat v0.1.0 h1:uvBfDaB3eaIjNYXCl0qYnDqykodvkVDXk5g2fIssGiI=
136140
github.com/metno/forti-internalformat v0.1.0/go.mod h1:91hkl5tZDcNeZKHkw72u6EFGyiZWZWv7ijxX9R4mmtI=
141+
github.com/metno/go-weathersymbol v0.1.1 h1:IgYHGkTFL28i0mlBo9b5BkF19+a3/pLxwz+tW7JwyOI=
142+
github.com/metno/go-weathersymbol v0.1.1/go.mod h1:YisSf88s5ZtgpZuKW4LgEB6IanDuCBt3lXfh4JB/diw=
137143
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
138144
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
145+
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
146+
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
139147
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
140148
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
141149
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
@@ -178,6 +186,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
178186
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
179187
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
180188
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
189+
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
190+
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
181191
gocloud.dev v0.46.0 h1:niIuZwSjMtBx8K+ITB2s5kZullB13PGOS2ZoQPZxQ4Q=
182192
gocloud.dev v0.46.0/go.mod h1:ACQe+2qO+hEO+pdcvvsM+RB63r8TyGD1W3ESCLFyzvM=
183193
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"log"
8+
9+
"github.com/invopop/jsonschema"
10+
"github.com/metno/forti/jsonfrontend/pkg/jsonformat"
11+
)
12+
13+
func main() {
14+
version := flag.String("version", "", "semantic version to embed in the schema $id (e.g. v1.0.0)")
15+
flag.Parse()
16+
17+
schema := jsonschema.Reflect(&jsonformat.GeoJSON{})
18+
19+
if *version != "" {
20+
schema.ID = jsonschema.ID(fmt.Sprintf(
21+
"https://raw.githubusercontent.com/metno/forti/forecast.schema.%s.json",
22+
*version,
23+
))
24+
}
25+
26+
data, err := json.MarshalIndent(schema, "", " ")
27+
if err != nil {
28+
log.Fatal(err)
29+
}
30+
fmt.Println(string(data))
31+
}

jsonfrontend/pkg/jsonformat/geojson.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import (
66
)
77

88
type GeoJSON struct {
9-
Type string `json:"type"`
9+
Type string `json:"type" jsonschema:"enum=Feature"`
1010
Geometry Geometry `json:"geometry"`
1111
Properties *Forecast `json:"properties,omitempty"`
1212
}
1313

1414
type GeoJSONCoordinate float32
1515
type Geometry struct {
16-
Type string `json:"type"`
17-
Coordinates []GeoJSONCoordinate `json:"coordinates"`
16+
Type string `json:"type" jsonschema:"enum=Point"`
17+
Coordinates []GeoJSONCoordinate `json:"coordinates" jsonschema:"minItems=2,maxItems=3,description=Longitude and latitude, with optional altitude as a third element"`
1818
}
1919

2020
func (c GeoJSONCoordinate) MarshalJSON() ([]byte, error) {

jsonfrontend/pkg/jsonformat/json.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,25 @@ type Forecast struct {
1111
}
1212

1313
type Metadata struct {
14-
UpdatedAt time.Time `json:"updated_at"`
15-
Error string `json:"error,omitempty"`
16-
Units map[string]string `json:"units"`
17-
RadarCoverage string `json:"radar_coverage,omitempty"`
14+
UpdatedAt time.Time `json:"updated_at" jsonschema:"description=Time of the last forecast update"`
15+
Error string `json:"error,omitempty" jsonschema:"description=Error message if the forecast is unavailable"`
16+
Units map[string]string `json:"units" jsonschema:"description=Maps each forecast parameter name to its physical unit"`
17+
RadarCoverage string `json:"radar_coverage,omitempty" jsonschema:"description=Custom metadata used only for timeseries forecasts from radar data. Radar coverage status for the location"`
1818
}
1919

2020
type TimeStep struct {
21-
Time time.Time `json:"time"`
22-
Data map[string]TimestepData `json:"data"`
21+
Time time.Time `json:"time" jsonschema:"description=Start time of the forecast interval"`
22+
Data map[string]TimestepData `json:"data" jsonschema:"description=Forecast data keyed by interval length e.g. instance, next_1_hours"`
2323
}
2424

2525
type TimestepData struct {
26-
Summary *Summary `json:"summary,omitempty"`
27-
Details ForecastDetails `json:"details"`
26+
Summary *Summary `json:"summary,omitempty" jsonschema:"description=Summary of the forecast parameters"`
27+
Details ForecastDetails `json:"details" jsonschema:"description=List of forecast parameters and their numerical values"`
2828
}
2929

3030
type Summary struct {
31-
SymbolCode string `json:"symbol_code"`
32-
SymbolConfidence string `json:"symbol_confidence,omitempty"`
31+
SymbolCode string `json:"symbol_code" jsonschema:"description=Weather symbol identifier"`
32+
SymbolConfidence string `json:"symbol_confidence,omitempty" jsonschema:"description=Confidence level of the weather symbol"`
3333
}
3434

3535
type ForecastDetails map[string]SingleDigitFloat

0 commit comments

Comments
 (0)