Question

How to Send JSON in a POST Request in Go

gojsonrestapiary.io

Question

I am trying to send JSON to a mock server using Go. The JSON payload may be different on every request, so I cannot rely on a fixed struct.

Here is the code I am using:

package main

import (
    "encoding/json"
    "fmt"
    "github.com/jmcvetta/napping"
    "log"
    "net/http"
)

func main() {
    url := "http://restapi3.apiary.io/notes"
    fmt.Println("URL:>", url)

    s := napping.Session{}
    h := &http.Header{}
    h.Set("X-Custom-Header", "myvalue")
    s.Header = h

    jsonStr := []byte(`{ "title": "Buy cheese and bread for breakfast."}`)

    var data map[string]json.RawMessage
    err := json.Unmarshal(jsonStr, &data)
    if err != nil {
        fmt.Println(err)
    }

    resp, err := s.Post(url, &data, nil, nil)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("response Status:", resp.Status())
    fmt.Println("response Headers:", resp.HttpResponse().Header)
    fmt.Println("response Body:", resp.RawText())
}

This does not send the JSON correctly, and I am not sure why. How should I send a JSON string in a POST request in Go when the JSON structure is dynamic and may change from one request to another?

Short Answer

By the end of this page, you will understand how to send JSON in a POST request in Go, especially when the payload is dynamic. You will learn when to use raw JSON bytes, map[string]any, or structs, how to set the correct headers, and how to avoid common mistakes when building HTTP requests.

Concept

Sending JSON in a Go POST request

When you send JSON to an API, the server expects two main things:

  • A request body containing valid JSON text
  • A Content-Type: application/json header

In Go, there are a few common ways to provide that JSON body:

  • Structs: best when the shape of the JSON is known in advance
  • Maps: useful when the JSON is dynamic
  • Raw JSON bytes or strings: useful when you already have a JSON string and want to send it as-is

In the original code, the JSON string is first unmarshaled into map[string]json.RawMessage, and then passed to a helper library. That can work in some situations, but it is often unnecessary if you already have valid JSON text. If your payload changes on every request, you usually have two simple choices:

  1. Send the raw JSON string directly as the request body
  2. Build a map[string]any, then marshal it into JSON before sending

Why this matters

Real programs send JSON all the time:

  • Calling REST APIs
  • Sending webhook payloads
  • Posting form-like data in JSON format
  • Communicating between services

If the body is sent incorrectly, the server may:

  • Reject the request
  • Treat it as plain text instead of JSON
  • Parse an unexpected structure
  • Return confusing errors like 400 Bad Request or 415 Unsupported Media Type

The key idea is simple: the HTTP request body must contain JSON bytes, and the headers must say that the body is JSON.

Mental Model

Think of an HTTP POST request like mailing a package:

  • The body is the item inside the box
  • The headers are the label on the outside
  • Content-Type: application/json is the label that says: "This box contains JSON"

If you put JSON-like data into the box but forget the label, or if you transform the content into the wrong format before sending it, the receiver may not know how to open it correctly.

If your JSON changes every time, that is fine. You do not need a custom box for every package. You can either:

  • Put the finished JSON text directly into the box, or
  • Build the content dynamically, then convert it to JSON before sealing the box
Take Quiz

Syntax and Examples

Option 1: Send a raw JSON string directly

If you already have valid JSON text, send it as the request body.

package main

import (
    "bytes"
    "fmt"
    "io"
    "log"
    "net/http"
)

func main() {
    url := "http://restapi3.apiary.io/notes"
    jsonStr := []byte(`{"title":"Buy cheese and bread for breakfast."}`)

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
    if err != nil {
        log.Fatal(err)
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-Custom-Header", "myvalue")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)

    fmt.Println("Status:", resp.Status)
    fmt.Println("Body:", string(body))
}

Why this works

  • jsonStr already contains valid JSON
  • bytes.NewBuffer(jsonStr) turns it into a request body
  • Content-Type tells the server to parse the body as JSON

Option 2: Build dynamic JSON using a map

If the JSON changes often, a map is a good choice.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
)

func main() {
    url := "http://restapi3.apiary.io/notes"

    payload := map[string]any{
        "title": "Buy cheese and bread for breakfast.",
        "done":  false,
        "count": 1,
    }

    jsonData, err := json.Marshal(payload)
    if err != nil {
        log.Fatal(err)
    }

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
    if err != nil {
        log.Fatal(err)
    }

    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)

    fmt.Println("Status:", resp.Status)
    fmt.Println("Body:", string(body))
}

Why this works

  • The map lets you create flexible fields at runtime
  • json.Marshal converts the Go map into valid JSON bytes
  • The marshaled bytes are sent as the request body

If you want to keep using napping

If a library expects a body object, you usually pass either:

  • A Go value to be marshaled into JSON, or
  • Raw bytes/string, depending on library support

A simpler dynamic approach would be:

payload := map[string]any{
    "title": "Buy cheese and bread for breakfast.",
}

resp, err := s.Post(url, &payload, nil, nil)

But if you already have a JSON string, using Go's standard net/http package is often the clearest approach.

Step by Step Execution

Example

payload := map[string]any{
    "title": "Buy cheese",
    "done":  false,
}

jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)

What happens step by step

1. Create a Go value

payload := map[string]any{
    "title": "Buy cheese",
    "done":  false,
}

This creates a dynamic data structure in Go.

2. Convert it to JSON

jsonData, _ := json.Marshal(payload)

Now Go turns the map into bytes like this:

{"done":false,"title":"Buy cheese"}

The key order may vary, which is normal in JSON.

3. Build the HTTP request

req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))

This creates a POST request and places the JSON bytes into the body.

4. Add the JSON header

req.Header.Set("Content-Type", "application/json")

This tells the server how to interpret the body.

5. Send the request

resp, _ := http.DefaultClient.Do(req)

The request is sent over the network.

6. The server receives

The server sees:

  • HTTP method: POST
  • Header: Content-Type: application/json
  • Body: JSON text

That is the complete recipe for a valid JSON POST request.

Real World Use Cases

Common uses for JSON POST requests in Go

Calling REST APIs

A Go backend might create users, orders, or support tickets by sending JSON to another service.

map[string]any{
    "email": "user@example.com",
    "role": "admin",
}

Sending webhook events

Applications often notify other systems by POSTing JSON payloads.

Examples:

  • Payment completed
  • User signed up
  • Build finished

Uploading dynamic form data

If users can submit custom fields, a fixed struct may not fit. A map is often easier.

Testing mock servers

When working with tools like Apiary, Postman mocks, or local test servers, sending raw JSON helps verify request formats.

Microservice communication

One service may serialize data into JSON and POST it to another service for processing.

Take Quiz

Real Codebase Usage

How this is used in real projects

Developers rarely hardcode everything inline. Instead, they use patterns that make JSON requests safer and easier to maintain.

Validation before sending

Before marshaling or sending JSON, code often checks required values.

if title == "" {
    return fmt.Errorf("title is required")
}

Early returns on errors

Go code commonly exits as soon as an error occurs.

jsonData, err := json.Marshal(payload)
if err != nil {
    return err
}

Helper functions for API calls

Many codebases wrap repeated request logic.

func postJSON(url string, payload any) error {
    jsonData, err := json.Marshal(payload)
    if err != nil {
        return err
    }

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
    if err != nil {
        return err
    }

    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return fmt.Errorf("request failed: %s", resp.Status)
    }

    return nil
}

Using maps for flexible payloads

When payload fields are optional or dynamic, developers often build a map[string]any and conditionally add keys.

payload := map[string]any{}
payload["title"] = title
if note != "" {
    payload["note"] = note
}

Logging response details

In integrations, developers log:

  • Status code
  • Response body
  • Request payload

This helps debug failed API calls.

Timeouts and custom clients

Production code usually avoids the default client without timeouts.

client := &http.Client{Timeout: 10 * time.Second}

This prevents requests from hanging forever.

Common Mistakes

1. Forgetting the Content-Type header

If you do not set this header, the server may not treat the body as JSON.

Broken

req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))

Correct

req.Header.Set("Content-Type", "application/json")

2. Unmarshaling JSON just to send it again

If you already have valid JSON text, you usually do not need to unmarshal it first.

Unnecessary

var data map[string]json.RawMessage
json.Unmarshal(jsonStr, &data)

Simpler

req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))

3. Using json.RawMessage without a real need

json.RawMessage is mainly useful when you want to delay parsing or keep nested raw JSON fragments. It is not the usual starting point for a simple POST body.

4. Ignoring errors

Beginners often use _ and skip error handling.

Broken

jsonData, _ := json.Marshal(payload)

Better

jsonData, err := json.Marshal(payload)
if err != nil {
    return err
}

5. Assuming dynamic JSON means structs are impossible

If the whole payload is unpredictable, a map is fine. But many real payloads are only partly dynamic. You can mix approaches:

  • Use a struct for known fields
  • Use a map for extra fields

6. Confusing Go values with JSON text

This Go value:

map[string]any{"title": "Buy cheese"}

is not JSON yet.

It must be marshaled first:

json.Marshal(payload)

7. Not closing the response body

Always close the body after a request.

defer resp.Body.Close()

Comparisons

Common ways to send JSON in Go

ApproachBest forProsCons
Struct + json.MarshalKnown JSON shapeType-safe, clear, easy to maintainLess flexible for changing fields
map[string]any + json.MarshalDynamic JSONFlexible and simpleLess type safety
Raw JSON string/bytesAlready have JSON textNo extra conversion neededEasier to send invalid JSON by mistake
json.RawMessageEmbedding or delaying JSON parsingUseful for advanced JSON handlingOverkill for basic POST requests

map[string]any vs struct

Featuremap[string]anyStruct
Dynamic fieldsExcellentLimited
Type safetyWeakStrong
ReadabilityModerateHigh
Best for APIs with fixed schemaOkayBest
Best for unknown payloadsBestPoor

net/http vs helper library

OptionWhen to use itNotes
net/httpMost casesStandard library, clear, flexible
Third-party libraryIf it simplifies repeated workGood if you understand how it serializes bodies

For learning and debugging, net/http is often the best place to start because it shows exactly what is being sent.

Take Quiz

Cheat Sheet

Quick reference

Send an existing JSON string

jsonStr := []byte(`{"title":"Buy cheese"}`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)

Send dynamic JSON from a map

payload := map[string]any{
    "title": "Buy cheese",
    "done":  false,
}

jsonData, err := json.Marshal(payload)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)

Important rules

  • JSON must go in the request body
  • Set Content-Type to application/json
  • Use json.Marshal for Go maps or structs
  • If you already have valid JSON text, send it directly
  • Close resp.Body with defer resp.Body.Close()
  • Check errors at every step

Good types to use

  • struct for fixed JSON
  • map[string]any for dynamic JSON
  • []byte or string for raw JSON text

Avoid

  • Unmarshaling JSON just to send it again
  • Forgetting headers
  • Ignoring errors
  • Using json.RawMessage for simple cases unless you really need it

FAQ

Why is my Go POST request not sending JSON correctly?

Usually because the request body is not raw JSON bytes, or the Content-Type: application/json header is missing.

Can I send JSON in Go without using a struct?

Yes. You can use map[string]any for dynamic payloads or send a raw JSON string directly.

Do I need to call json.Unmarshal before sending JSON?

No. If you already have valid JSON text, send it directly. Unmarshal is for parsing incoming JSON, not for sending it.

What is the difference between json.Marshal and json.Unmarshal?

Marshal converts Go values into JSON. Unmarshal converts JSON into Go values.

Should I use json.RawMessage for POST requests?

Usually not for simple requests. It is more useful when you need to keep part of the JSON unparsed or embed raw JSON inside another structure.

Is map[string]any better than a struct for APIs?

Only when the payload is dynamic. If the API schema is stable, structs are usually safer and easier to maintain.

Can I use the standard library instead of a third-party package?

Yes. net/http is often the simplest and clearest way to send JSON in Go.

What header should I set when posting JSON in Go?

Set:

Content-Type: application/json
Take Quiz

Mini Project

Description

Build a small Go program that sends a JSON note to an API endpoint. The project demonstrates both a dynamic payload and a proper JSON POST request using the standard library. This is useful because many real integrations need to send changing JSON data to APIs, webhooks, or mock servers.

Goal

Create a Go program that builds a dynamic JSON payload, sends it in a POST request, and prints the response status and body.

Requirements

  • Create a JSON payload using a Go map
  • Convert the payload to JSON before sending
  • Send the request with the Content-Type: application/json header
  • Print the HTTP response status
  • Read and print the response body
Take Quiz

Keep learning

Related questions

View more