Question
I want to handle a JSON POST request in Go, but the approach I currently have feels wrong.
I am sending this request:
curl -X POST -d '{"test": "that"}' http://localhost:8082/test
And here is my Go code:
package main
import (
"encoding/json"
"log"
"net/http"
)
type testStruct struct {
Test string
}
func test(rw http.ResponseWriter, req *http.Request) {
req.ParseForm()
log.Println(req.Form)
// LOG: map[{"test": "that"}:[]]
var t testStruct
for key := range req.Form {
log.Println(key)
// LOG: {"test": "that"}
err := json.Unmarshal([]byte(key), &t)
if err != nil {
log.Println(err.Error())
}
}
log.Println(t.Test)
// LOG: that
}
func main() {
http.HandleFunc("/test", test)
log.Fatal(http.ListenAndServe(":8082", nil))
}
This works, but it seems very hacky because I am calling ParseForm() and then extracting the JSON from a form key.
What is the proper, idiomatic way to accept and decode JSON in a Go POST request?
Short Answer
By the end of this page, you will understand why JSON request data in Go should be read from req.Body instead of req.Form, how to decode it using encoding/json, and what an idiomatic JSON handler looks like in real Go applications.
Concept
JSON sent in an HTTP POST request is usually placed in the request body, not in the URL query string and not in form fields.
In Go's net/http package, these kinds of request inputs are handled differently:
req.URL.Query()reads query parameters from the URL.req.ParseForm()reads form data such asapplication/x-www-form-urlencodedormultipart/form-data.req.Bodycontains the raw body of the request, which is where JSON payloads belong.
That means if a client sends JSON, the normal pattern is:
- Define a Go struct that matches the expected JSON shape.
- Read or decode the JSON from
req.Body. - Handle any decoding errors.
- Use the decoded struct values in your handler.
In your current code, ParseForm() is interpreting the raw JSON body as form data, which is why the entire JSON string ends up as a key. That is not what ParseForm() is designed for.
In real applications, handling JSON correctly matters because APIs, webhooks, frontend apps, and mobile clients commonly send JSON payloads. If you decode directly from the body, your code becomes cleaner, safer, and easier to maintain.
Mental Model
Think of an HTTP request as having different compartments:
- The URL is like the address on an envelope.
- Query parameters are little notes written on the outside.
- Form fields are like a paper form filled with named boxes.
- The body is the actual document placed inside the envelope.
JSON is usually the document inside the envelope.
So if you try to read JSON using ParseForm(), it is like trying to extract a typed letter by scanning checkbox fields on a form. You might accidentally get something usable, but you are using the wrong tool for the job.
The correct approach is to open the envelope and read the document inside: req.Body.
Syntax and Examples
The idiomatic way to decode JSON from a request body in Go is to use json.NewDecoder(req.Body).
package main
import (
"encoding/json"
"log"
"net/http"
)
type TestStruct struct {
Test string `json:"test"`
}
func test(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
return
}
var t TestStruct
err := json.NewDecoder(req.Body).Decode(&t)
if err != nil {
http.Error(rw, "invalid JSON", http.StatusBadRequest)
return
}
log.Println(t.Test)
rw.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/test", test)
log.Fatal(http.ListenAndServe(":8082", nil))
}
You can test it with:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"test":"that"}' \
http://localhost:8082/test
Why the struct tag matters
Step by Step Execution
Consider this handler:
func test(rw http.ResponseWriter, req *http.Request) {
var t TestStruct
err := json.NewDecoder(req.Body).Decode(&t)
if err != nil {
http.Error(rw, "invalid JSON", http.StatusBadRequest)
return
}
log.Println(t.Test)
}
And this request:
curl -X POST -H "Content-Type: application/json" -d '{"test":"that"}' http://localhost:8082/test
Here is what happens step by step:
- The client sends an HTTP
POSTrequest. - The JSON text
{"test":"that"}is placed in the request body. - Go receives the request and makes the body available through
req.Body. json.NewDecoder(req.Body)creates a JSON decoder that reads directly from the body stream..Decode(&t)parses the JSON and fills thetstruct.- The JSON key
testis mapped to the Go fieldTestbecause of the struct tag`json:"test"`.
Real World Use Cases
Handling JSON request bodies is common in many Go programs:
- REST APIs: creating users, orders, comments, or products.
- Frontend-to-backend communication: JavaScript apps often send JSON with
fetchoraxios. - Webhooks: external services send event payloads as JSON.
- CLI tools: local tools may send JSON to a local Go server.
- Microservices: one service sends structured JSON data to another.
Example API request for creating a user:
{
"name": "Ava",
"email": "ava@example.com"
}
Example Go struct:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
This pattern appears in almost every Go API codebase.
Real Codebase Usage
In real Go projects, developers usually combine JSON decoding with a few common patterns.
Guard clauses
Return early if the request method or input is wrong.
if req.Method != http.MethodPost {
http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
return
}
Content-Type checks
Some handlers verify that the client is sending JSON.
if req.Header.Get("Content-Type") != "application/json" {
http.Error(rw, "content type must be application/json", http.StatusUnsupportedMediaType)
return
}
In practice, some clients send values like application/json; charset=utf-8, so exact string matching may be too strict.
Input validation
Decode first, then validate required fields.
if t.Test == "" {
http.Error(rw, "test is required", http.StatusBadRequest)
return
}
Decoding into request-specific structs
Developers usually create a small struct for each endpoint instead of using generic maps everywhere.
LoginRequest {
Email
Password
}
Common Mistakes
1. Using ParseForm() for JSON
Broken example:
req.ParseForm()
log.Println(req.Form)
Why it is wrong:
ParseForm()is for form submissions, not JSON bodies.- JSON should be decoded from
req.Body.
Fix:
var t TestStruct
err := json.NewDecoder(req.Body).Decode(&t)
2. Forgetting struct tags
Broken example:
type TestStruct struct {
test string
}
Problems:
testis unexported because it starts with a lowercase letter.- Unexported fields cannot be filled by
encoding/json.
Fix:
type TestStruct struct {
Test string `json:"test"`
}
3. Ignoring decode errors
Comparisons
| Approach | Use for | Good for JSON? | Notes |
|---|---|---|---|
req.ParseForm() | Form submissions and query data | No | Designed for form-encoded data, not raw JSON |
req.URL.Query() | URL query parameters | No | Reads values from the URL only |
json.NewDecoder(req.Body).Decode(&v) | JSON request bodies | Yes | Idiomatic for handlers |
io.ReadAll(req.Body) + json.Unmarshal | JSON request bodies | Yes | Useful if you need the raw body too |
json.NewDecoder vs
Cheat Sheet
// Define expected JSON
type TestStruct struct {
Test string `json:"test"`
}
// Decode JSON body
var t TestStruct
err := json.NewDecoder(req.Body).Decode(&t)
if err != nil {
http.Error(rw, "invalid JSON", http.StatusBadRequest)
return
}
Rules to remember
- JSON request data is read from
req.Body. - Form data is read with
ParseForm(). - Struct fields must be exported to be decoded.
- Use JSON tags like
`json:"test"`for predictable mapping. - Always handle decode errors.
- Usually check the request method before decoding.
req.Bodyis a stream and is usually read once.
Common curl command
curl -X POST \
-H "Content-Type: application/json" \
-d '{"test":"that"}' \
http://localhost:8082/test
Strict decoding
decoder := json.NewDecoder(req.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&t)
JSON response
FAQ
How do I read JSON from an HTTP request in Go?
Use json.NewDecoder(req.Body).Decode(&yourStruct).
Should I use ParseForm() for JSON in Go?
No. ParseForm() is for form data, not JSON request bodies.
Why is my struct not being filled during JSON decoding?
The fields may be unexported, or your JSON keys may not match. Use exported fields and JSON tags.
Do I need to set Content-Type: application/json?
Yes, clients should send it for JSON requests. Many servers also validate it.
What is the difference between json.Unmarshal and json.NewDecoder?
Unmarshal works on a byte slice you already have. NewDecoder reads directly from an io.Reader, such as req.Body.
Can I decode JSON into a map instead of a struct?
Yes, for example map[string]interface{} or map[string]string, but structs are usually clearer and safer.
How do I return JSON from a Go handler?
Set the response to and use .
Mini Project
Description
Build a small Go endpoint that accepts a JSON POST request containing a message and returns a JSON response confirming what was received. This demonstrates the correct way to decode JSON from req.Body, validate input, and send JSON back to the client.
Goal
Create a /echo endpoint that accepts JSON input, validates it, and responds with JSON.
Requirements
- Create a Go HTTP server listening on port
8080 - Add a
POST /echoroute - Accept a JSON body with a
messagefield - Return
400 Bad Requestif the JSON is invalid or the message is empty - Return a JSON response containing the received message
Keep learning
Related questions
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Check if a Value Exists in a Slice in Go
Learn how to check whether a value exists in a slice in Go, and why Go has no Python-style `in` operator for arrays or slices.
Concatenating Slices in Go with append
Learn how to concatenate two slices in Go using append and the ... operator, with examples, pitfalls, and practical usage.