Fix JsonMarshalError: json: unsupported type: chan int in Gin
This error occurs when Gin tries to serialize a response containing a type that encoding/json cannot marshal, such as channels, functions, or complex numbers. Fix it by creating a dedicated response DTO struct that only contains JSON-serializable fields and mapping your internal types to it before calling c.JSON().
Reading the Stack Trace
Here's what each line means:
- encoding/json.unsupportedTypeEncoder(0x140001a8000, {0x1028e4f20, 0x14000196040, 0x194}): The JSON encoder encounters a type it cannot serialize (chan, func, or complex) and raises an error.
- main.GetJob(0x14000226000) /app/handlers/job.go:31 +0x1c4: The GetJob handler at line 31 passes the internal Job struct directly to c.JSON, including the un-serializable channel field.
- github.com/gin-gonic/gin.(*Context).JSON(0x14000226000, 0xc8, {0x102850ea0, 0x14000196040}): Gin's JSON method calls encoding/json.Marshal which fails on the unsupported type.
Common Causes
1. Struct contains a channel field
The struct passed to c.JSON has a chan field that encoding/json cannot serialize.
type Job struct {
ID int `json:"id"`
Name string `json:"name"`
Done chan bool `json:"done"`
}
func GetJob(c *gin.Context) {
job := fetchJob(c.Param("id"))
c.JSON(200, job) // fails: chan bool not serializable
}
2. Struct contains a func field
A callback or handler function stored on the struct cannot be marshaled to JSON.
type Task struct {
ID int `json:"id"`
OnDone func() error `json:"on_done"`
}
func GetTask(c *gin.Context) {
c.JSON(200, task)
}
3. Circular reference in struct
Two structs reference each other, causing infinite recursion during JSON marshaling.
type Parent struct {
Child *Child `json:"child"`
}
type Child struct {
Parent *Parent `json:"parent"`
}
The Fix
Create a separate response DTO struct that only contains JSON-serializable types. Map the internal struct fields to the DTO, converting non-serializable types like channels into meaningful string representations.
type Job struct {
ID int `json:"id"`
Name string `json:"name"`
Done chan bool `json:"done"`
}
func GetJob(c *gin.Context) {
job := fetchJob(c.Param("id"))
c.JSON(200, job)
}
type Job struct {
ID int
Name string
Done chan bool
}
type JobResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
func GetJob(c *gin.Context) {
job := fetchJob(c.Param("id"))
resp := JobResponse{
ID: job.ID,
Name: job.Name,
Status: checkStatus(job.Done),
}
c.JSON(http.StatusOK, resp)
}
func checkStatus(done chan bool) string {
select {
case <-done:
return "completed"
default:
return "running"
}
}
Testing the Fix
package handlers_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestGetJob_ReturnsValidJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/jobs/:id", GetJob)
req := httptest.NewRequest(http.MethodGet, "/api/jobs/42", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp JobResponse
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(t, err)
assert.Equal(t, 42, resp.ID)
}
func TestCheckStatus_Running(t *testing.T) {
ch := make(chan bool)
assert.Equal(t, "running", checkStatus(ch))
}
func TestCheckStatus_Completed(t *testing.T) {
ch := make(chan bool, 1)
close(ch)
assert.Equal(t, "completed", checkStatus(ch))
}
Run your tests:
go test ./handlers/... -v
Pushing Through CI/CD
git checkout -b fix/gin-json-marshaling-error,git add handlers/job.go handlers/job_test.go,git commit -m "fix: use response DTO to avoid marshaling unsupported types",git push origin fix/gin-json-marshaling-error
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go mod download
- run: go vet ./...
- run: go test ./... -race -coverprofile=coverage.out
- run: go build ./...
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
go get github.com/bugstack/sdk
Step 2: Initialize
import "github.com/bugstack/sdk"
func init() {
bugstack.Init(os.Getenv("BUGSTACK_API_KEY"))
}
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Run go test ./... locally to confirm the fix passes.
- Open a pull request with the DTO changes.
- Wait for CI checks to pass on the PR.
- Have a teammate review and approve the PR.
- Merge to main and verify the deployment in staging before promoting to production.
Frequently Asked Questions
BugStack runs the fix through your existing test suite, generates additional edge-case tests, and validates that the JSON responses match expected schemas before marking it safe to deploy.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
Channels, functions, and complex numbers cannot be marshaled. Circular references also cause issues. Use json:"-" tags or separate DTOs to exclude them.
json:"-" works for simple cases, but a dedicated response struct is cleaner for APIs because it decouples your internal model from the API contract.