Gofinity
instructions45 / 63
taught in: json and encoding
Mediumjsonstructsmapserrorstypes~30m+100 XP

Decoding JSON

Decoding is the other half of encoding/json, and it is the half with the surprises. The plain form:

var task Task
err := json.Unmarshal(data, &task)

The & is not optional. Unmarshal writes through that pointer, so passing the value itself is an InvalidUnmarshalError rather than a silent no-op. If a decode leaves you with a zero value and no error, check this first.

Decoding is a fill, not a check

Two behaviours follow from that, and both are on purpose:

  • A field the input does not mention is left at its zero value. There is no way to tell "absent" from "present and zero" with a plain field - a *string is the usual fix, and the next challenge uses one.
  • A field the input mentions that the struct does not have is ignored. A typo in a key is silence, not an error.

Matching is also case-insensitive: "title", "Title" and "TITLE" all land in Title. An exact tag match wins over a case-insensitive one, but with no tags at all, any casing gets in.

What is an error: malformed JSON, and a value of the wrong type. {"ID":"one"} into an int field is an UnmarshalTypeError.

When you do not know the shape

Decode into map[string]any and every value arrives as one of exactly six Go types:

JSONGo
objectmap[string]any
array[]any
stringstring
numberfloat64
true / falsebool
nullnil

That fourth row is the one that catches people. There are no integers here. 42 comes back as a float64 holding 42, and getting an int out of it is an assertion followed by a conversion:

f, ok := m["count"].(float64)
if !ok {
	return 0, false
}
return int(f), true

Two comma-ok forms stacked: one for the map lookup, one for the assertion. Skipping either is a panic waiting for the first input you did not write yourself. (When integer precision actually matters - json.Decoder.UseNumber - there is a json.Number for it.)

The Decoder

json.Unmarshal takes a []byte that must be exactly one JSON value. json.NewDecoder(r) reads from an io.Reader and unlocks two things Unmarshal cannot do.

Strict fields. Turn the silence above into an error:

decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err := decoder.Decode(&task)

Streams. Decode reads one value per call and leaves the reader positioned after it, so a sequence of concatenated objects is a loop. It returns io.EOF when the stream is spent, and that is the only error that means "done":

for {
	var task Task
	err := decoder.Decode(&task)
	if errors.Is(err, io.EOF) {
		return tasks, nil
	}
	if err != nil {
		return nil, err
	}
	tasks = append(tasks, task)
}

Decoding straight from an http.Response.Body is the same call, which is why you see it far more often than Unmarshal in real code.

Task

Implement DecodeTask, DecodeAll, DecodeAny, LookupInt, DecodeStrict and DecodeStream.

Hints

  • DecodeAll gets the nil-versus-empty behaviour for free: declare var tasks []Task and pass &tasks. null leaves it nil, [] makes it empty and non-nil.
  • DecodeStrict needs a reader over the bytes it was handed: bytes.NewReader(data).
  • DecodeStream builds its slice with make([]Task, 0) so an empty stream returns empty rather than nil.
  • Use errors.Is(err, io.EOF) for the end of the stream, and return every other error.