Typed JSON: Decode & MapAny
TinyGo can't reliably use encoding/json or reflect. So Gothic generates the decoder at build time: Decode[T] reads your struct's fields and json: tags and turns a Response into a typed value — no reflection, no runtime cost. MapAny is the untyped escape hatch when you just want a quick peek.
The own-BFF pattern. Your API route emits JSON; your WASM client decodes it into the same struct the server marshalled. One type, two importers, zero drift.
Rule that makes or breaks the build: the struct T must live in a net/http-free package. Because Decode[T] pulls T's package into the WASM, TinyGo compiles it — and TinyGo cannot build net/http. So a struct sitting in your api package (whose handlers take http.ResponseWriter) will fail the build. Put shared DTOs in a leaf package like src/shared: struct declarations only.
// src/shared/models.go — a plain, net/http-free package.
// Imported by BOTH your API handler and your WASM page: one source of truth.
package shared
type Address struct {
City string `json:"city"`
Zip string `json:"zip"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Username string `json:"user_name"` // snake_case: key != Go field
Tags []string `json:"tags"`
Address Address `json:"address"` // nested struct
}Now decode into that struct. The type argument is explicit and required — Decode[shared.User](resp), never Decode(resp). Missing keys and null become the zero value; nested structs, slices, and pointers all work; unknown keys are ignored.
import (
routes "github.com/gothicframework/core/router"
. "github.com/gothicframework/core/wasm"
"github.com/your-org/your-app/src/shared"
)
CreateWasmFunc("loadUser", func() {
resp, err := Fetch("/api/user/1")
if err != nil {
SetText("out", "error: "+err.Error())
return
}
// Decode[T] is a free function (Go forbids generic methods).
// The type argument is REQUIRED and must be a struct.
u, err := Decode[shared.User](resp)
if err != nil {
SetText("out", "decode error: "+err.Error())
return
}
SetText("name", u.Name) // typed field
SetText("user", u.Username) // json:"user_name" rename
SetText("city", u.Address.City) // nested struct
SetText("first-tag", u.Tags[0]) // slice element
SetText("age", strconv.Itoa(u.Age))
})No struct handy? resp.MapAny() parses an object into a map[string]any. Note: numbers decode as float64 (an int64 above 2^53 loses precision), so cast accordingly.
CreateWasmFunc("peek", func() {
resp, err := Fetch("/api/user/1")
if err != nil {
return
}
// MapAny is untyped: JSON object -> map[string]any. Numbers are float64.
// Great for a quick read when you don't have (or don't want) a struct.
m, err := resp.MapAny()
if err != nil {
return
}
if name, ok := m["name"].(string); ok {
SetText("name", name)
}
if age, ok := m["age"].(float64); ok {
SetText("age", strconv.Itoa(int(age)))
}
})Decoding is only half the story. Want to send a typed struct as a request body? Learn Encode next!
