Weather JSON to Go structs
With Weather as the Root name, the sample weather response becomes:
{
"location": { "name": "Reykjavik", "lat": 64.1466, "lon": -21.9426 },
"current": {
"temp_c": 3.2,
"wind_kph": 24.1,
"condition": { "text": "Light rain", "code": 1183 }
},
"forecast": [
{
"date": "2026-11-06",
"max_c": 5.1,
"min_c": -0.4,
"chance_of_rain": 80
},
{
"date": "2026-11-07",
"max_c": 4.4,
"min_c": -1.2,
"chance_of_rain": 35
}
]
}
package main
type Weather struct {
Location Location `json:"location"`
Current Current `json:"current"`
Forecast []Forecast `json:"forecast"`
}
type Forecast struct {
Date string `json:"date"`
MaxC float64 `json:"max_c"`
MinC float64 `json:"min_c"`
ChanceOfRain int64 `json:"chance_of_rain"`
}
type Current struct {
TempC float64 `json:"temp_c"`
WindKph float64 `json:"wind_kph"`
Condition Condition `json:"condition"`
}
type Condition struct {
Text string `json:"text"`
Code int64 `json:"code"`
}
type Location struct {
Name string `json:"name"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
Fields are exported because encoding/json only fills exported fields, and each tag keeps the original key: temp_c becomes TempC with json:"temp_c". Both forecast days merge into one Forecast struct.
int64 or float64
A field is int64 when every sample value is whole, and float64 once one has a decimal part. chance_of_rain was 80 and 35, so it is int64; if the API can send 12.5, make it float64 or json.Unmarshal fails on that field.
Pointers, omitempty and initialisms
After array items are merged (rules on JSON to TypeScript), fields that are null or missing somewhere become pointers with omitempty. With Nodes as the root name:
[
{ "id": 1, "ip": "10.0.0.4", "uptime_sec": 86400, "api_url": "https://n1.example.com" },
{ "id": 2, "ip": "10.0.0.9", "uptime_sec": null, "region": "eu-west" }
]
package main
type Nodes []Node
type Node struct {
ID int64 `json:"id"`
IP string `json:"ip"`
UptimeSec *int64 `json:"uptime_sec,omitempty"`
APIURL *string `json:"api_url,omitempty"`
Region *string `json:"region,omitempty"`
}
A plain int64 can’t say “not sent”: a missing or null value leaves 0. A nil *int64 can, and omitempty drops it when you marshal. Check before you dereference: if n.Region != nil { fmt.Println(*n.Region) }. Slices and any need no pointer, since nil already means nothing there. Optional objects become pointers too, because omitempty never treats a struct value as empty; Go 1.24’s omitzero does, but older versions lack it.
Initialisms follow Go style: id, ip and api_url become ID, IP and APIURL. The generator knows about two dozen, including URL, HTTP, JSON and UUID.
Decode it
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
func main() {
data, err := os.ReadFile("weather.json")
if err != nil {
log.Fatal(err)
}
var w Weather
if err := json.Unmarshal(data, &w); err != nil {
log.Fatal(err)
}
fmt.Println(w.Location.Name, w.Forecast[0].MaxC)
}
Change package main to your own package. json.Unmarshal skips unknown keys; to reject them, decode with json.NewDecoder and call DisallowUnknownFields() first. invalid character ‘<’ looking for beginning of value means the server sent HTML instead of JSON. JSON to Rust does the same job with serde.