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
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/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 bodyContent-Typetells 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.Marshalconverts 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.
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
| 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
| Feature | map[string]any | Struct |
|---|---|---|
| Dynamic fields | Excellent | Limited |
| Type safety | Weak | Strong |
| Readability | Moderate | High |
| Best for APIs with fixed schema | Okay | Best |
| Best for unknown payloads | Best | Poor |
net/http vs helper library
| Option | When to use it | Notes |
|---|---|---|
net/http | Most cases | Standard library, clear, flexible |
| Third-party library | If it simplifies repeated work | Good 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.
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
structfor fixed JSONmap[string]anyfor dynamic JSON[]byteorstringfor raw JSON text
Avoid
- Unmarshaling JSON just to send it again
- Forgetting headers
- Ignoring errors
- Using
json.RawMessagefor 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
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
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.