Treat the request body as untrusted bytes. A Content-Type header is a declaration, not proof that the bytes are valid or the title is useful.
The create contract accepts one object with one title string. It rejects unknown fields, duplicate keys (including escaped spellings), trailing JSON, invalid UTF-8, controls and titles outside the declared length range. The native exercise supplies the strict schema parser; you implement bounded reading and error propagation.
Read at most 1,025 bytes with io.LimitReader. A 1,024-byte body is allowed if it otherwise validates; receiving the extra byte proves the declared limit was exceeded. Do not read the entire stream merely to discover its size.
A reader error differs from a size error. Both return a zero CreateInput; the handler maps them into stable client-facing error categories without echoing raw payloads.
package main
import ("fmt";"io";"strings")
func main(){
const limit=4
b,_:=io.ReadAll(io.LimitReader(strings.NewReader("abcdef"),limit+1));fmt.Println(len(b),len(b)>limit)
}Go’s JSON decoder can accept duplicate object keys and replace invalid UTF-8 by default. That is insufficient for this stricter API contract. The supplied token-based helper records whether title has appeared, rejects other keys, validates UTF-8 before decoding and checks that the stream ends after one object.
The title policy counts Unicode code points after trimming, not raw UTF-8 bytes. The byte-size bound still applies to the entire encoded document. A string is not accepted merely because it has the correct Go type; semantic length/control checks remain necessary.
Validation is separate from storage. No note ID should be consumed and no partial title should escape when input fails. Later handler tests verify that failed requests leave the repository unchanged.
package main
import ("fmt";"encoding/json")
func main(){
var value any;err:=json.Unmarshal([]byte(`{"title":null}`),&value);fmt.Println(err==nil,value.(map[string]any)["title"]==nil)
}Complete the bounded read, then run duplicate-key, invalid-UTF-8, over-limit, exact-limit and reader-error cases. Notice which supporting parser checks are supplied and which IO boundary you wrote.
Next we make successful and failed responses obey one JSON representation contract.
Follow one request through the service.
These seven exchanges were generated by the downloadable Go replay against one fresh in-memory store. Choose a recorded step to inspect its request, response and resulting note count. Choosing a step does not send a live request.
- Client requestused
- Route & methodused
- Bounded decoderused
- Repositoryused
- JSON responseused
Request
POST /notes
Content-Type: application/json{
"title": " learn "
}Recorded response
{
"Content-Length": "25",
"Content-Type": "application/json; charset=utf-8",
"Location": "/notes/1",
"X-Content-Type-Options": "nosniff"
}{
"id": 1,
"title": "learn"
}Headers, status, body and stored count are actual recorder results. The highlighted stages annotate the corresponding code path; this is a replay, not a distributed trace. The duplicate-key step leaves the stored count at one.
Download the runnable service, replay and tests →