Question

How to Handle JSON POST Requests in Go

gojson

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 as application/x-www-form-urlencoded or multipart/form-data.
  • req.Body contains the raw body of the request, which is where JSON payloads belong.

That means if a client sends JSON, the normal pattern is:

  1. Define a Go struct that matches the expected JSON shape.
  2. Read or decode the JSON from req.Body.
  3. Handle any decoding errors.
  4. 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.

Take Quiz

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

type TestStruct struct {
    Test string `json:"test"`
}

The field name in Go is Test, but the JSON key is test. The struct tag tells the JSON decoder how to map the JSON field to the Go struct field.

Another valid approach

You could also read the raw body first, then unmarshal it:

body, err := io.ReadAll(req.Body)
if err != nil {
    http.Error(rw, "failed to read body", http.StatusBadRequest)
    return
}

var t TestStruct
err = json.Unmarshal(body, &t)
if err != nil {
    http.Error(rw, "invalid JSON", http.StatusBadRequest)
    return
}

This works, but json.NewDecoder(req.Body) is usually simpler for request handlers.

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:

  1. The client sends an HTTP POST request.
  2. The JSON text {"test":"that"} is placed in the request body.
  3. Go receives the request and makes the body available through req.Body.
  4. json.NewDecoder(req.Body) creates a JSON decoder that reads directly from the body stream.
  5. .Decode(&t) parses the JSON and fills the t struct.
  6. The JSON key test is mapped to the Go field Test because of the struct tag `json:"test"`.
  7. After decoding, t.Test contains the value "that".
  8. log.Println(t.Test) prints:
that

If the request body contains invalid JSON, Decode returns an error and the handler should respond with a 400 Bad Request.

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 fetch or axios.
  • 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.

Take Quiz

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.

type LoginRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

This makes code easier to read and safer to refactor.

Returning JSON responses

JSON APIs often decode JSON in and encode JSON out.

rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{
    "status": "ok",
})

Disallowing unknown fields

If you want stricter input handling:

decoder := json.NewDecoder(req.Body)
decoder.DisallowUnknownFields()

var t TestStruct
if err := decoder.Decode(&t); err != nil {
    http.Error(rw, "invalid JSON", http.StatusBadRequest)
    return
}

This helps catch typos in client requests.

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:

  • test is 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

Broken example:

json.NewDecoder(req.Body).Decode(&t)
log.Println(t.Test)

Why it is dangerous:

  • Invalid JSON may silently leave fields empty.
  • Your handler may continue with bad input.

Fix:

if err := json.NewDecoder(req.Body).Decode(&t); err != nil {
    http.Error(rw, "invalid JSON", http.StatusBadRequest)
    return
}

4. Not checking the HTTP method

If your endpoint should only accept POST, enforce it.

if req.Method != http.MethodPost {
    http.Error(rw, "method not allowed", http.StatusMethodNotAllowed)
    return
}

5. Assuming JSON keys must match Go field case exactly

The decoder uses exported Go fields and optional struct tags. The safest practice is to always define JSON tags.

6. Trying to read the body multiple times

req.Body is a stream. Once it has been fully read, it is usually empty.

Broken pattern:

body1, _ := io.ReadAll(req.Body)
body2, _ := io.ReadAll(req.Body)

The second read will not contain the original JSON.

Comparisons

ApproachUse forGood for JSON?Notes
req.ParseForm()Form submissions and query dataNoDesigned for form-encoded data, not raw JSON
req.URL.Query()URL query parametersNoReads values from the URL only
json.NewDecoder(req.Body).Decode(&v)JSON request bodiesYesIdiomatic for handlers
io.ReadAll(req.Body) + json.UnmarshalJSON request bodiesYesUseful if you need the raw body too

json.NewDecoder vs json.Unmarshal

MethodInputTypical use
json.NewDecoder(req.Body).Decode(&v)Stream / readerBest for HTTP request bodies
json.Unmarshal(data, &v)[]byteBest when you already have bytes in memory

Form POST vs JSON POST

TypeContent-TypeHow to read in Go
Form POSTapplication/x-www-form-urlencodedreq.ParseForm() / req.FormValue()
Multipart formmultipart/form-datareq.ParseMultipartForm()
JSON POSTapplication/jsonjson.NewDecoder(req.Body).Decode(...)
Take Quiz

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.Body is 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

rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"message": "ok"})

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 Content-Type to application/json and use json.NewEncoder(rw).Encode(...).

Take Quiz

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 /echo route
  • Accept a JSON body with a message field
  • Return 400 Bad Request if the JSON is invalid or the message is empty
  • Return a JSON response containing the received message
Take Quiz

Keep learning

Related questions

View more