How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
Gin · Go

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

2024/03/15 15:10:22 [GIN] 2024/03/15 - 15:10:22 | 500 | 2.104ms | 127.0.0.1 | GET /api/jobs/42 goroutine 34 [running]: runtime/debug.Stack() /usr/local/go/src/runtime/debug/stack.go:24 +0x5e encoding/json.(*encodeState).marshal.func1() /usr/local/go/src/encoding/json/encode.go:307 +0x64 encoding/json.unsupportedTypeEncoder(0x140001a8000, {0x1028e4f20, 0x14000196040, 0x194}) /usr/local/go/src/encoding/json/encode.go:469 +0x48 encoding/json.(*encodeState).reflectValue(0x140001a8000, {0x1028e4f20, 0x14000196040, 0x194}, {0x0}) /usr/local/go/src/encoding/json/encode.go:358 +0x7c encoding/json.Marshal({0x102850ea0, 0x14000196040}) /usr/local/go/src/encoding/json/encode.go:164 +0x70 github.com/gin-gonic/gin.(*Context).JSON(0x14000226000, 0xc8, {0x102850ea0, 0x14000196040}) /go/pkg/mod/github.com/gin-gonic/gin@v1.9.1/context.go:1024 +0x38 main.GetJob(0x14000226000) /app/handlers/job.go:31 +0x1c4

Here's what each line means:

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.

Before (broken)
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)
}
After (fixed)
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:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. 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:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. 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)

  1. Run go test ./... locally to confirm the fix passes.
  2. Open a pull request with the DTO changes.
  3. Wait for CI checks to pass on the PR.
  4. Have a teammate review and approve the PR.
  5. 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.