How to Use the encoding.TextMarshaler and encoding.TextUnmarshaler Interfaces

Implement MarshalText and UnmarshalText methods on your type to control how it converts to and from text for JSON and other encoders.

The universal translator for your types

You build a custom type to wrap a value with behavior. Maybe it's a Port that validates ranges, a Color that formats as hex, or a Money amount that tracks currency. The type works great in memory. Then you try to save it to a JSON config, pass it as a command-line flag, or store it in a database column. Suddenly, the serialization libraries don't know your rules. They dump raw fields, ignore validation, or produce output that breaks your parsers.

Go solves this with a pair of interfaces in the encoding package. Implement encoding.TextMarshaler and encoding.TextUnmarshaler, and you get a universal translator. The encoding/json package, the flag package, and many third-party libraries check for these interfaces automatically. You write the text conversion logic once. The ecosystem respects it everywhere.

How the contract works

The interfaces define a simple exchange. TextMarshaler asks your type to produce a byte slice representing itself. TextUnmarshaler asks your type to parse a byte slice and update its state.

type TextMarshaler interface {
    MarshalText() (text []byte, err error)
}

type TextUnmarshaler interface {
    UnmarshalText(text []byte) error
}

The return type is []byte, not string. This is a performance convention in Go. Byte slices avoid the allocation overhead of creating immutable strings in hot paths. The text is expected to be UTF-8 encoded. If your type cannot represent itself as text, or if the input text is invalid, return an error. The caller decides how to handle the failure.

These interfaces live in encoding, not in json. That placement signals they are a general-purpose contract. Any package that deals with text-based serialization can use them. You are not coupling your type to JSON. You are declaring how your type looks as text.

Minimal example: a validated port number

Here's a Port type that wraps an integer. It marshals to a string like "8080" and validates the range during unmarshaling.

package main

import (
    "fmt"
    "strconv"
)

// Port wraps an integer to enforce valid port ranges during text parsing.
type Port int

// MarshalText converts the port to a byte slice.
// The receiver is a value because we only read the field.
func (p Port) MarshalText() ([]byte, error) {
    // WHY: Convert the int to a string, then to bytes for the interface contract.
    return []byte(strconv.Itoa(int(p))), nil
}

// UnmarshalText parses a byte slice into the port and validates the range.
// The receiver is a pointer so we can modify the Port value.
func (p *Port) UnmarshalText(text []byte) error {
    // WHY: Pointer receiver allows writing the parsed value back to the struct.
    n, err := strconv.Atoi(string(text))
    if err != nil {
        // WHY: Wrap the error to provide context about which field failed.
        return fmt.Errorf("invalid port number: %w", err)
    }
    // WHY: Enforce the valid TCP/UDP port range.
    if n < 0 || n > 65535 {
        return fmt.Errorf("port out of range: %d", n)
    }
    *p = Port(n)
    return nil
}

The receiver naming follows Go convention. The receiver name is p, matching the type Port. We don't use this or self. Short names keep the code readable and align with standard library style. Run gofmt to ensure the formatting matches community expectations. The tool decides indentation and spacing. Trust it.

Walkthrough: receivers and errors

The method signatures reveal two critical design choices. MarshalText uses a value receiver. UnmarshalText uses a pointer receiver.

MarshalText only reads the struct fields. A value receiver is sufficient and safer. It prevents accidental mutation during serialization. The compiler allows a value receiver here because the method doesn't need to change the object.

UnmarshalText must modify the struct. A pointer receiver is mandatory. If you use a value receiver for UnmarshalText, the compiler accepts the code because the method set satisfies the interface. However, the unmarshaling writes to a temporary copy. The original value remains unchanged. This is a silent logic error. The code compiles, but the data never updates. Always use a pointer receiver for UnmarshalText.

Error handling follows the standard Go pattern. Check errors immediately and wrap them with context. The if err != nil { return err } boilerplate is verbose by design. It makes the unhappy path visible. Wrapping with %w preserves the error chain for callers who use errors.Is or errors.As.

If you mess up the return type, the compiler catches it. Returning a string instead of a byte slice produces cannot use s (variable of type string) as []byte value in return argument. Returning no error when the interface requires one gives MarshalText method has incorrect signature. The compiler enforces the contract strictly.

Realistic example: JSON and flags

The power of these interfaces appears when other packages use them. encoding/json checks for TextMarshaler before serializing a value. If the interface is present, json.Marshal calls MarshalText, takes the result, and wraps it in JSON quotes.

package main

import (
    "encoding/json"
    "fmt"
)

// Config holds application settings with a custom Port type.
type Config struct {
    Name string `json:"name"`
    Port Port   `json:"port"`
}

func main() {
    c := Config{Name: "api-server", Port: 8080}
    
    // WHY: json.Marshal detects TextMarshaler and uses it automatically.
    data, err := json.Marshal(c)
    if err != nil {
        panic(err)
    }
    
    // WHY: The output shows the port as a quoted string, not a raw number.
    fmt.Println(string(data))
    // Output: {"name":"api-server","port":"8080"}
}

The JSON output contains "port":"8080". The port is a string in the JSON. This is the behavior of TextMarshaler. If you need the port to appear as a JSON number, do not implement TextMarshaler. Leave the field as an int or implement json.Marshaler instead. TextMarshaler forces the text representation, which becomes a JSON string.

The flag package also respects TextUnmarshaler. If you register a custom flag type that implements the interface, flag.Parse calls UnmarshalText with the command-line argument. You get validation for free. The same interface powers JSON, flags, and many config parsers. Implement the contract once. The ecosystem does the rest.

Pitfalls and common mistakes

The pointer receiver trap is the most common bug. Developers implement UnmarshalText with a value receiver, test it on a local variable, and see it work. Then they use the type in a struct, unmarshal JSON, and find the field is zero. The unmarshaler modified a copy. The fix is switching to a pointer receiver.

Another pitfall is mixing TextMarshaler with json.Marshaler. If a type implements both, json.Marshaler takes precedence for JSON. json.Marshal checks for json.Marshaler first. If found, it calls that method and ignores TextMarshaler. Use TextMarshaler when the text representation is the canonical form and you want it to work across multiple packages. Use json.Marshaler only when you need JSON-specific behavior, like returning a JSON object instead of a string.

Be careful with nil slices. MarshalText should return a valid byte slice, even if empty. Returning nil can confuse some callers that expect a slice. Return []byte("") for empty text.

Validation belongs in UnmarshalText. This is the right place to reject bad input. If the text doesn't match your rules, return an error. The caller gets a clear message. Don't silently accept invalid data and fix it later. Fail fast.

Decision: when to use which interface

Choose the interface based on how your type needs to serialize and which packages you want to support.

Use encoding.TextMarshaler and encoding.TextUnmarshaler when your type has a canonical text representation that multiple packages should respect, such as JSON, TOML, YAML, or command-line flags.

Use json.Marshaler and json.Unmarshaler when you need to control the exact JSON structure, such as returning a JSON object, embedding metadata, or producing a number instead of a string.

Use standard struct fields when the type is a simple container and the default serialization matches your needs.

Use fmt.Stringer when you only need a human-readable string for logging or printing, and do not care about data exchange or parsing.

The interfaces are tools. Pick the one that matches your serialization goal. Don't implement json.Marshaler just to change a field name. Use a json tag. Don't implement TextMarshaler just to validate input. Use UnmarshalText for validation during parsing.

TextMarshaler is the universal translator. Write it once, use it everywhere. Pointer receivers for unmarshaling. Value receivers for marshaling. Check your receivers before you debug a silent failure.

Where to go next