---
title: Fix: "json: unsupported type" in Go
summary: Fix the 'json: unsupported type' error by removing functions, channels, or complex interfaces from your struct before marshaling to JSON.
lang: en
slug: fix-json-unsupported-type-in-go
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.gofaq.org/en/fix-json-unsupported-type-in-go/
---

# Fix: "json: unsupported type" in Go



## The error means you're trying to serialize behavior, not data

You're building a config loader. You have a struct with a `Name` and a `Handler` function. You call `json.Marshal` to save it to disk. The program crashes with `json: unsupported type: func()`. You stare at the code. The struct looks fine. The function works fine. Why does the JSON encoder hate your function?

JSON is a data format. It represents numbers, strings, objects, arrays, booleans, and null. It has no concept of executable code. It has no concept of runtime communication channels. It has no concept of complex numbers. The `encoding/json` package converts Go values to JSON bytes. It walks your Go values and maps them to JSON types. When it hits something that doesn't map, it stops and reports the error.

Think of JSON as a photograph. A photograph captures the appearance of an object. It captures color, shape, and position. It cannot capture the weight of the object. It cannot capture the sound the object makes. It cannot capture the instructions for building the object. Functions are instructions. Channels are communication pipes. You can photograph a book, but you cannot photograph the act of reading. The encoder is the camera. It takes a picture of your data. When you hand it a function, it has nothing to photograph.

## How the encoder walks your values

The `json.Marshal` function uses reflection to inspect your value. It first checks if the value implements the `json.Marshaler` interface. The interface defines a single method: `MarshalJSON() ([]byte, error)`. If your type implements this method, the encoder calls it and trusts the result. This is the "accept interfaces, return structs" pattern in action. The encoder accepts the interface to customize behavior. You return a struct or bytes to provide the data.

If the value doesn't implement the interface, the encoder falls back to default behavior. It looks at the underlying kind of the value. Strings, numbers, and booleans map directly. Structs become objects. Slices become arrays. Pointers are dereferenced. The encoder checks the kind of the dereferenced value. Functions, channels, and complex numbers do not map. The encoder returns an error immediately. It does not skip the field. It does not write `null`. It fails the entire operation.

The error message names the concrete type that caused the failure. If you have a function, the error is `json: unsupported type: func()`. If you have a channel, the error is `json: unsupported type: chan string`. The message helps you locate the unsupported field.

JSON is data. Functions are behavior. You can't serialize behavior.

## Minimal example: the function trap

```go
package main

import (
	"encoding/json"
	"fmt"
)

// Config holds application settings.
type Config struct {
	Name   string
	OnLoad func() // Functions cannot be serialized to JSON.
}

func main() {
	c := Config{
		Name:   "my-app",
		OnLoad: func() { fmt.Println("loaded") },
	}

	// Marshal attempts to convert the struct to JSON bytes.
	data, err := json.Marshal(c)
	if err != nil {
		// The encoder rejects the function with json: unsupported type: func().
		fmt.Println("Error:", err)
		return
	}
	fmt.Println(string(data))
}
```

The encoder walks the `Config` struct. It sees `Name` and writes a string. It sees `OnLoad` and sees a function pointer. JSON has no representation for executable code. The encoder returns an error. The program prints the error and exits.

## Realistic fix: implementing MarshalJSON

You often need to serialize a struct that contains runtime state. A `Task` might have a channel for progress updates. A `Client` might have a mutex for thread safety. You want to serialize the metadata but exclude the runtime state. Implementing `MarshalJSON` gives you full control.

```go
package main

import (
	"encoding/json"
	"fmt"
)

// Task represents a unit of work.
type Task struct {
	ID      int
	Payload string
	// StatusChannel sends updates when the task completes.
	// Channels are unsupported by the JSON encoder.
	StatusChannel chan string
}

// MarshalJSON implements json.Marshaler for Task.
// It returns a JSON representation that excludes the channel.
func (t Task) MarshalJSON() ([]byte, error) {
	// Define an alias to avoid infinite recursion.
	type Alias Task

	// Create a temporary struct with only serializable fields.
	return json.Marshal(struct {
		Alias
		// StatusChannel is omitted intentionally.
	}{
		Alias: Alias(t),
	})
}

func main() {
	t := Task{
		ID:            42,
		Payload:       "process data",
		StatusChannel: make(chan string, 1),
	}

	data, err := json.Marshal(t)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println(string(data))
}
```

The `MarshalJSON` method returns a JSON representation of the task. It defines an alias type `Alias` that has the same fields as `Task` but no methods. This breaks the cycle. If you called `json.Marshal(t)` inside the method, it would call `MarshalJSON` again. The recursion would continue until the stack overflows. The alias prevents this. The method constructs a temporary struct that embeds the alias. The temporary struct has no `StatusChannel` field. The encoder serializes the safe struct. The output contains `ID` and `Payload` but no `StatusChannel`.

The receiver name is `t`. Go convention uses one or two letters matching the type. The method is public because the `json.Marshaler` interface requires a public method. The error handling follows the standard pattern. The `if err != nil` check is verbose by design. It makes the unhappy path visible.

Implement `MarshalJSON` to control the output. The alias trick prevents infinite loops.

## Pitfalls: interfaces, map keys, and hidden types

Interface values can hide unsupported types. If you have a field of type `interface{}` and you assign a function to it, the encoder sees the interface. It checks the dynamic type. It finds a function. It fails. The error message is `json: unsupported type: func()`. The error tells you the concrete type, not the interface type. This helps you find the culprit.

Map keys have restrictions. JSON object keys must be strings. Go maps can have any comparable type as key. The encoder tries to convert map keys to strings. Integer keys become strings automatically. A map with key `123` becomes `{"123": "value"}`. Struct keys do not convert. If you have a `map[struct{}]string`, the encoder fails with `json: unsupported type: struct {...}`. Map keys must be strings, numbers, or types that can be converted to strings. Complex types as keys are forbidden.

Pointers dereference automatically. A `*string` is supported. The encoder dereferences the pointer and serializes the string. A `*func()` is not supported. The encoder dereferences the pointer and finds a function. It fails. The error is `json: unsupported type: func()`. Pointers don't save you from unsupported types.

Check the concrete type in the error message. Interfaces hide the truth until runtime.

## Decision: how to handle unsupported types

Use the `json:"-"` tag when a field should never be serialized or deserialized. This is the simplest fix for fields like mutexes, channels, or function pointers that exist only for runtime behavior. The tag tells the encoder to ignore the field completely.

Use a custom `MarshalJSON` method when you need to transform the data before serialization. This works when you want to include some fields but exclude others, or when you need to compute a derived value for the JSON output. The method gives you full control over the JSON structure.

Use an alias type inside `MarshalJSON` when you need to avoid infinite recursion. Defining `type Alias MyType` breaks the cycle so you can embed the safe fields without triggering the custom marshaler again. This is the standard pattern for custom marshaling.

Use a separate DTO struct when the serialization logic is complex. Create a plain struct with only the fields you need, copy the data over, and marshal the DTO. This keeps your domain model clean and avoids reflection overhead in hot paths.

Use `json.Marshal` on a subset of fields when you only need partial serialization. Construct a temporary struct or map with the specific values you want to send, rather than trying to filter a large struct at runtime.

Tag it out or transform it. Don't fight the encoder.

## Where to go next

- [How to Parse XML in Go with encoding/xml](/en/how-to-parse-xml-in-go-with-encodingxml/)
- [Fix: "json: cannot unmarshal X into Go value of type Y"](/en/fix-json-cannot-unmarshal-x-into-go-value-of-type-y/)
- [How to Pretty-Print JSON in Go](/en/how-to-pretty-print-json-in-go/)