SuperProdfor Kubenetes
Guide

Custom Decoders

Implementing custom type decoders

master

Overview

Custom decoders let you control exactly how envconfig deserializes an environment variable string into any Go type. By implementing a small interface on your type, you can handle formats that envconfig does not natively support — such as IP addresses, JSON-encoded structures, or domain-specific encodings. This page shows you how to implement the envconfig.Decoder interface, when to use it instead of built-in type support, and how to integrate custom decoders into your configuration structs.


Prerequisites

Before you begin, make sure you have:

  • Go 1.13 or later installed
  • The envconfig library added to your module (see installation steps below)
  • Familiarity with Go interfaces and pointer receivers
  • A basic envconfig struct already set up — if not, complete the basic usage workflow first

Installation
  1. Add envconfig to your Go module:
go get github.com/kelseyhightower/envconfig
  1. Import it in any file where you define configuration structs or custom decoders:
import "github.com/kelseyhightower/envconfig"

No additional dependencies are required for custom decoders — the Decoder interface is defined within the envconfig package itself.


Configuration

envconfig recognizes two interfaces that give a type control over its own deserialization:

InterfaceMethod signatureWhen to use
envconfig.DecoderDecode(value string) errorPrimary choice for custom type parsing
flag.ValueSet(value string) errorUse when your type already satisfies the standard library flag.Value interface

envconfig checks for Decoder first. If your type (or a pointer to your type) implements Decode(string) error, envconfig calls that method with the raw environment variable string and expects your implementation to populate the receiver. Return a non-nil error to signal that the value is invalid — envconfig will propagate this error from envconfig.Process.

Struct tag options work normally alongside custom decoders:

TagEffect
`envconfig:"VAR_NAME"`Map the field to a specific environment variable name
`default:"value"`Supply a fallback string passed to Decode when the variable is unset
`required:"true"`Return an error from Process if the variable is absent

Define Decode on a pointer receiver so the method can mutate the value in place.


Usage

Implementing envconfig.Decoder

Create a named type (or alias an existing type) and attach a Decode(string) error method with a pointer receiver:

import (
    "net"
    "github.com/kelseyhightower/envconfig"
)

type IPDecoder net.IP

func (ipd *IPDecoder) Decode(value string) error {
    *ipd = IPDecoder(net.ParseIP(value))
    return nil
}

Then use your type as a field in any envconfig struct. The struct tag envconfig:"DNS_SERVER" maps the field to that exact environment variable name:

type DNSConfig struct {
    Address IPDecoder `envconfig:"DNS_SERVER"`
}

When you call envconfig.Process, envconfig reads the environment variable string and delegates parsing entirely to your Decode method:

var cfg DNSConfig
if err := envconfig.Process("", &cfg); err != nil {
    log.Fatal(err)
}

Using flag.Value as an alternative

If your type already implements the flag.Value interface (i.e., it has a Set(string) error method), envconfig will call Set in the same way. No changes to your struct definition are needed — envconfig detects the interface automatically. Use Decoder when you are writing a type specifically for envconfig; use flag.Value when reusing a type shared with the flag package.


Examples

Example 1 — Decoding an IP address

Parse a plain IP address string into a net.IP-backed type.

export DNS_SERVER=8.8.8.8
package main

import (
    "fmt"
    "log"
    "net"

    "github.com/kelseyhightower/envconfig"
)

type IPDecoder net.IP

func (ipd *IPDecoder) Decode(value string) error {
    *ipd = IPDecoder(net.ParseIP(value))
    return nil
}

type DNSConfig struct {
    Address IPDecoder `envconfig:"DNS_SERVER"`
}

func main() {
    var cfg DNSConfig
    if err := envconfig.Process("", &cfg); err != nil {
        log.Fatal(err)
    }
    fmt.Println(net.IP(cfg.Address)) // 8.8.8.8
}

Expected output:

8.8.8.8

Example 2 — Decoding a complex structured type (map of JSON arrays)

This example shows how to handle a rich, domain-specific encoding that built-in support cannot manage — a semicolon-delimited list of key=JSON pairs decoded into map[string][]providerDetails.

export SMS_PROVIDER_WITH_WEIGHT='IND=[{"name":"SMSProvider1","weight":70},{"name":"SMSProvider2","weight":30}];US=[{"name":"SMSProvider1","weight":100}]'
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "strings"

    "github.com/kelseyhightower/envconfig"
)

type providerDetails struct {
    Name   string `json:"name"`
    Weight int    `json:"weight"`
}

type SMSProviderDecoder map[string][]providerDetails

func (sd *SMSProviderDecoder) Decode(value string) error {
    result := map[string][]providerDetails{}
    for _, pair := range strings.Split(value, ";") {
        kv := strings.SplitN(pair, "=", 2)
        if len(kv) != 2 {
            return fmt.Errorf("invalid map item: %q", pair)
        }
        var providers []providerDetails
        if err := json.Unmarshal([]byte(kv[1]), &providers); err != nil {
            return fmt.Errorf("invalid map json: %w", err)
        }
        result[kv[0]] = providers
    }
    *sd = SMSProviderDecoder(result)
    return nil
}

type SMSProviderConfig struct {
    ProviderWithWeight SMSProviderDecoder `envconfig:"SMS_PROVIDER_WITH_WEIGHT"`
}

func main() {
    var cfg SMSProviderConfig
    if err := envconfig.Process("", &cfg); err != nil {
        log.Fatal(err)
    }
    for region, providers := range cfg.ProviderWithWeight {
        fmt.Printf("%s:\n", region)
        for _, p := range providers {
            fmt.Printf("  %s (weight %d)\n", p.Name, p.Weight)
        }
    }
}

Expected output:

IND:
  SMSProvider1 (weight 70)
  SMSProvider2 (weight 30)
US:
  SMSProvider1 (weight 100)

Troubleshooting

Decode is never called

Symptom: The environment variable is set, but your Decode method does not appear to run — the field stays at its zero value.

Likely cause: The method is defined on a value receiver instead of a pointer receiver, so envconfig cannot detect the interface.

Fix: Change the receiver to a pointer:

// Wrong
func (ipd IPDecoder) Decode(value string) error { ... }

// Correct
func (ipd *IPDecoder) Decode(value string) error { ... }

envconfig.Process returns an error from inside Decode

Symptom: Process returns an error message that originates inside your Decode implementation.

Likely cause: The environment variable string is not in the format your decoder expects (wrong delimiter, malformed JSON, etc.).

Fix: Print or log the raw variable value before parsing to confirm what the decoder received:

fmt.Println(os.Getenv("YOUR_VAR"))

Then update either the environment variable value or your parsing logic to match.


Field is ignored even though the environment variable is set

Symptom: The struct field stays empty despite the variable being exported and correctly named.

Likely cause 1: The field has an ignored:"true" struct tag. envconfig skips it unconditionally.

Fix: Remove or set the tag to ignored:"false".

Likely cause 2: The envconfig:"..." tag value does not match the actual environment variable name (case-sensitive).

Fix: Verify that the tag value exactly matches the exported variable name, including underscores and case:

// Tag and variable must match exactly
type Config struct {
    Address IPDecoder `envconfig:"DNS_SERVER"`
}
// export DNS_SERVER=8.8.8.8  ✓
// export dns_server=8.8.8.8  ✗

Default value is not passed to Decode

Symptom: You set a default:"..." tag, the environment variable is unset, but Decode is still not called with the default.

Likely cause: This is expected behavior only if your type does not implement Decoder. If it does implement Decoder, envconfig will pass the default string to Decode. Confirm that the method signature exactly matches Decode(string) error — a different signature means the interface is not satisfied and the field falls through to built-in handling.

Fix: Check the method signature and recompile. You can also add a quick compile-time assertion:

var _ envconfig.Decoder = (*IPDecoder)(nil)

This line will fail to compile if *IPDecoder does not implement envconfig.Decoder.