grpc/grpc-go · error
rls: json unmarshal failed for service config %+v
Error message
rls: json unmarshal failed for service config %+v: %v
What it means
Emitted from (rlsBB).ParseConfig (config.go:152) when the top-level JSON passed to the RLS balancer's ParseConfig does not unmarshal into the lbConfigJSON struct shape. json.Unmarshal into a struct fails on malformed JSON syntax, or on a type mismatch for one of the known fields (RouteLookupConfig / RouteLookupChannelServiceConfig / ChildPolicy / ChildPolicyConfigTargetFieldName). This is the first gate any RLS service config must pass.
Solutions
- Run the config through a JSON validator (e.g. 'jq empty cfg.json') to surface syntax errors.
- Confirm the top-level shape matches lbConfigJSON: an object with RouteLookupConfig (object), optional RouteLookupChannelServiceConfig (object), ChildPolicy (array of single-key objects), and ChildPolicyConfigTargetFieldName (string).
- If the config arrives via xDS, inspect the raw LDS/RDS resource dump (e.g. xds-resolver debug log or channelz) to see the bytes the resolver actually delivered.
- Regenerate the config from a known-good template or the RLS e2e test fixtures under balancer/rls/internal/test/e2e.
Example fix
// before: trailing comma / type mismatch
{
"loadBalancingConfig": [ { "rls": { "routeLookupConfig": { ... }, } } ]
}
// after: valid JSON, correct field types
{
"loadBalancingConfig": [
{ "rls": {
"routeLookupConfig": { ... },
"childPolicyConfigTargetFieldName": "serviceName",
"childPolicy": [ { "round_robin": {} } ]
} }
]
} Defensive patterns
Strategy: validation
Validate before calling
// Pre-validate that the RLS LB config unmarshals into the expected shape.
type lbConfigJSON struct {
RouteLookupConfig json.RawMessage
RouteLookupChannelServiceConfig json.RawMessage
ChildPolicy []map[string]json.RawMessage
ChildPolicyConfigTargetFieldName string
}
func validateRLSConfig(raw []byte) error {
var c lbConfigJSON
if err := json.Unmarshal(raw, &c); err != nil {
return fmt.Errorf("invalid RLS service config JSON: %w", err)
}
if len(c.RouteLookupConfig) == 0 {
return errors.New("routeLookupConfig is required")
}
return nil
} Type guard
func isParsableRLSConfig(raw []byte) bool {
var c struct{ RouteLookupConfig json.RawMessage }
return json.Unmarshal(raw, &c) == nil && len(c.RouteLookupConfig) > 0
} Prevention
- Pipe service configs through 'jq empty' in CI to catch syntax errors.
- Generate configs from typed templates rather than hand-editing JSON.
- Unit-test the config blob by invoking rls.ParseConfig in a Go test before deploying.
When it happens
Trigger: Calling balancer/rls ParseConfig (directly or via the gRPC service-config machinery / xDS RLS cluster specifier) with a JSON document that has a syntax error, or where RouteLookupConfig is not a JSON object/array of bytes, ChildPolicy is not a list of single-key maps, or ChildPolicyConfigTargetFieldName is not a string.
Common situations: Hand-authored service config JSON with a trailing comma, missing brace, or unquoted keys; a templating/YAML-to-JSON step that produced invalid JSON; passing the proto-encoded RLS config instead of the JSON-LB-config wrapper; an xDS control plane emitting a malformed RdsPlugin config.
Related errors
- rls: bad control channel service config
- rls: bad RouteLookupConfig proto %+v
- rls: cache_size_bytes must be set to a non-zero value: %+v
- rls: childPolicyConfigTargetFieldName field is not set in…
- rls: empty lookup_service in route lookup config %+v
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/5ae15a295950568e.
Report an issue: GitHub.
Appendix: source
Thrown at balancer/rls/config.go:152
// - `cache_size_bytes` field must have a value greater than 0, and if its
// value is greater than 5M, we cap it at 5M
//
// - routeLookupChannelServiceConfig:
// - if specified, must parse as valid service config
//
// - childPolicy:
// - must find a valid child policy with a valid config
//
// - childPolicyConfigTargetFieldName:
// - must be set and non-empty
func (rlsBB) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
if logger.V(2) {
logger.Infof("Received JSON service config: %v", pretty.ToJSON(c))
}
cfgJSON := &lbConfigJSON{}
if err := json.Unmarshal(c, cfgJSON); err != nil {
return nil, fmt.Errorf("rls: json unmarshal failed for service config %+v: %v", string(c), err)
}
m := protojson.UnmarshalOptions{DiscardUnknown: true}
rlsProto := &rlspb.RouteLookupConfig{}
if err := m.Unmarshal(cfgJSON.RouteLookupConfig, rlsProto); err != nil {
return nil, fmt.Errorf("rls: bad RouteLookupConfig proto %+v: %v", string(cfgJSON.RouteLookupConfig), err)
}
lbCfg, err := parseRLSProto(rlsProto)
if err != nil {
return nil, err
}
if sc := string(cfgJSON.RouteLookupChannelServiceConfig); sc != "" {
parsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(sc)
if parsed.Err != nil {
return nil, fmt.Errorf("rls: bad control channel service config %q: %v", sc, parsed.Err)
}
lbCfg.controlChannelServiceConfig = scView on GitHub (pinned to 0c51461d27)