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/jsonheader
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:
- Send the raw JSON string directly as the request body
- 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 Requestor415 Unsupported Media Type
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/jsonis 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
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
jsonStralready contains valid JSONbytes.NewBuffer(jsonStr)turns it into a request body
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.
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.
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)
err != {
err
}
resp.Body.Close()
resp.StatusCode >= {
fmt.Errorf(, resp.Status)
}
}
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.
Comparisons
Common ways to send JSON in Go
| Approach | Best for | Pros | Cons |
|---|---|---|---|
Struct + json.Marshal | Known JSON shape | Type-safe, clear, easy to maintain | Less flexible for changing fields |
map[string]any + json.Marshal | Dynamic JSON | Flexible and simple | Less type safety |
| Raw JSON string/bytes | Already have JSON text | No extra conversion needed | Easier to send invalid JSON by mistake |
json.RawMessage | Embedding or delaying JSON parsing | Useful for advanced JSON handling | Overkill for basic POST requests |
map[string]any vs struct
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-Typetoapplication/json - Use
json.Marshalfor Go maps or structs - If you already have valid JSON text, send it directly
- Close
resp.Bodywithdefer resp.Body.Close() - Check errors at every step
Good types to use
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. is often the simplest and clearest way to send JSON in Go.
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/jsonheader - Print the HTTP response status
- Read and print the response body
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.