#3881·kratos

Boolean config fields cannot be overridden from the environment

Author: jnicholsonwasabiCreated Aug 26, 2026Updated Aug 26, 2026
Labelsbug

What happened:

The kratos config/env Source emits every value as a string, and on Scan a string is rejected for a bool field. A single boolean override therefore aborts the config load:

json: cannot unmarshal string into Go struct field Bootstrap.debug of type bool

With a protobuf Bootstrap it is narrower and more surprising, because protojson accepts a quoted string for every other scalar kind. Only bool fails:

Override Result
log.level=debug (string) ok
...busy_timeout=5s (Duration) ok
...mode=MODE_FILE (enum) ok
...debug=true (bool) proto: invalid value for bool field debug: "true"

What you expected to happen:

APP_debug=true should set the field, the same way APP_level=debug already does.

Scan(v) already knows the destination, so the types are available without guessing them from the text. The natural fix is for the reader to coerce string leaves to the destination's type at scan time via the protobuf descriptor for a proto.Message, or the struct field type otherwise.

If that is too broad, an exported hook would be enough: a supported way for a Source or decoder to emit typed values, so this can be handled in a few lines outside the framework rather than by reimplementing the env source.

Please do not fix it by inferring types from the text alone ("true" → bool, "1234" → number).

That mistypes string fields whose values happen to look numeric, which is common for passwords, ports written as strings, and version numbers, and it fails on the deployment whose credentials happen to be digits rather than in development.

How to reproduce it (as minimally and precisely as possible):

Self-contained, no protoc required:

go
package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/go-kratos/kratos/v3/config"
	"github.com/go-kratos/kratos/v3/config/env"
	"github.com/go-kratos/kratos/v3/config/file"
)

type Bootstrap struct {
	Debug bool   `json:"debug"`
	Port  int    `json:"port"`
	Level string `json:"level"`
}

func load(dir string, opts ...config.Option) (Bootstrap, error) {
	opts = append([]config.Option{
		config.WithSource(file.NewSource(dir), env.NewSource("APP_")),
	}, opts...)

	c := config.New(opts...)
	defer c.Close()

	if err := c.Load(); err != nil {
		return Bootstrap{}, err
	}
	var bc Bootstrap
	return bc, c.Scan(&bc)
}

func main() {
	dir, _ := os.MkdirTemp("", "conf")
	defer os.RemoveAll(dir)
	os.WriteFile(
		filepath.Join(dir, "config.yaml"),
		[]byte("debug: false\nport: 8000\nlevel: info\n"),
		0o600,
	)

	report := func(name string, bc Bootstrap, err error) {
		if err != nil {
			fmt.Printf("%-32s FAIL  %v\n", name, err)
			return
		}
		fmt.Printf("%-32s ok    %+v\n", name, bc)
	}

	os.Setenv("APP_level", "debug")

	bs, err := load(dir)
	report("string override", bs, err)

	os.Unsetenv("APP_level")

	os.Setenv("APP_debug", "true")
	bs, err = load(dir)
	report("bool override", bs, err)

	bs, err = load(dir, config.WithResolveActualTypes(true))
	report("bool + WithResolveActualTypes", bs, err)
}

Produce output:

bash
$ # copy file above to main.go
$ go mod init kratos.repo
$ go mod tidy
$ go run ./main.go
string override                  ok    {Debug:false Port:8000 Level:debug}
2026/08/26 12:56:36 INFO watcher's ctx cancel error="context canceled"
2026/08/26 12:56:36 INFO watcher's ctx cancel error="context canceled"
2026/08/26 12:56:36 INFO watcher's ctx cancel error="context canceled"
bool override                    FAIL  json: cannot unmarshal string into Go struct field Bootstrap.debug of type bool
2026/08/26 12:56:36 INFO watcher's ctx cancel error="context canceled"
2026/08/26 12:56:36 INFO watcher's ctx cancel error="context canceled"
bool + WithResolveActualTypes    FAIL  json: cannot unmarshal string into Go struct field Bootstrap.debug of type bool
2026/08/26 12:56:36 INFO watcher's ctx cancel error="context canceled"
  string override                  ok    {Debug:false Port:8000 Level:debug}
  bool override                    FAIL  json: cannot unmarshal string into Go struct field Bootstrap.debug of type bool
  bool + WithResolveActualTypes    FAIL  json: cannot unmarshal string into Go struct field Bootstrap.debug of type bool

Anything else we need to know?:

Doesn't #3306 doesn't already cover this?

No, #3306 added convertToType, but it is only reachable from inside the loop over placeholder matches in expand:

go
  func expand(s string, mapping func(string) string, toType bool) any {
      re := placeholderRegexp.FindAllStringSubmatch(s, -1)
      var ct any
      for _, i := range re {
          if len(i) == 2 {
              m := mapping(i[1])
              if toType {
                  ct = convertToType(m)
                  return ct
              }
              s = strings.ReplaceAll(s, i[0], m)
          }
      }
      return s
  }

A plain env value such as true contains no ${...}, so re is empty, the body never runs, and the string is returned unchanged.

The conversion itself works, it is just gated behind placeholder syntax.

debug: ${DEBUG_FLAG:false} in the config file with WithResolveActualTypes(true) does produce a real bool. So the machinery already exists; it simply isn't applied to values arriving directly from a source.

Enabling that option is not a usable workaround, because it also changes two unrelated behaviors:

  • addr: "0.0.0.0:${PORT:8000}" becomes the number 8000. The return above discards the rest of the string, so interpolation stops working.

  • name: "${SVC:1234}" into a string field becomes the number 1234 and is rejected, because the conversion is by value shape rather than by destination type.

Environment:

  • Kratos version (use kratos -v): kratos version v3.0.0 (module github.com/go-kratos/kratos/v3 v3.0.0)
  • Go version (use go version): go version go1.26.5 darwin/arm64
  • OS (e.g: cat /etc/os-release): macOS 26.5.2 (build 25F84), Darwin 25.5.0 arm64