The same dot-shaped access hides two contracts. A struct column's shape is part of the table's type: the engine knows every field at plan time, so typos fail loudly, before data — the binder error naming candidates is the schema working for you. JSON defers to runtime: flexible for drift, silent for typos (payload.tempreture is NULL forever). Hence: JSON at the edges, structs in the modeled core, an explicit promotion step between.
SELECT id, {'depth': 4}.temp AS tempFROM ping;
SELECT id, {'depth': 4}.depth AS depth, json_extract('{"depth":4}', '$.temp') AS json_temp FROM ping;
Accessing a missing struct field raised a binder error before any data ran — a struct is typed nesting, so a typo fails at compile time; the same missing key in JSON returns a runtime NULL, silent forever.
JSON at edges, structs in the core, an explicit promotion step; struct typos are free tests — compile the query.