grpc/grpc-java · error · ResourceInvalidException

Custom LB config does not contain a JSON object

Error message

Custom LB config does not contain a JSON object

What it means

A custom LB policy config must be a JSON object (map) because it is passed to the named load balancing policy provider as a config map. If printing the Struct to JSON yields a scalar, array, or null instead of an object, the factory rejects the resource with this error.

Source

Thrown at xds/src/main/java/io/grpc/xds/LoadBalancerConfigFactory.java:375

        throws ResourceInvalidException {
      return ImmutableMap.of(parseCustomConfigTypeName(configTypedStruct.getTypeUrl()),
          (Map<String, ?>) parseCustomConfigJson(configTypedStruct.getValue()));
    }

    /**
     * Print the config Struct into JSON and then parse that into our internal representation.
     */
    private static Object parseCustomConfigJson(Struct configStruct)
        throws ResourceInvalidException {
      Object rawJsonConfig = null;
      try {
        rawJsonConfig = JsonParser.parse(JsonFormat.printer().print(configStruct));
      } catch (IOException e) {
        throw new ResourceInvalidException("Unable to parse custom LB config JSON", e);
      }

      if (!(rawJsonConfig instanceof Map)) {
        throw new ResourceInvalidException("Custom LB config does not contain a JSON object");
      }
      return rawJsonConfig;
    }


    private static String parseCustomConfigTypeName(String customConfigTypeName) {
      if (customConfigTypeName.contains("/")) {
        customConfigTypeName = customConfigTypeName.substring(
            customConfigTypeName.lastIndexOf("/") + 1);
      }
      return customConfigTypeName;
    }

    // Used to signal that the LB config goes too deep.
    static class MaxRecursionReachedException extends Exception {
      static final long serialVersionUID = 1L;
    }
  }

View on GitHub (pinned to 64daddc1f3)

Solutions

  1. Make the custom LB config a JSON object, e.g. {"field": value}, in the policy's custom_config on the control plane
  2. Confirm the policy name is set in the oneof name field, not inside the Struct
  3. Validate the emitted CDS/LDS JSON with an Envoy config dump before deploying

Example fix

// before (custom config as bare value)
"custom_config": "my-policy"
// after
"custom_config": {"someField": "someValue"}
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the custom config value is a JSON object before sending:
if (!(jsonNode.isObject())) {
  throw new IllegalArgumentException("custom_config must be a JSON object");
}

Try / catch

try {
  Map<String, ?> cfg = (Map<String, ?>) parseCustomConfigJson(struct);
} catch (ResourceInvalidException | ClassCastException e) {
  logger.warning("Custom LB config must be an object: " + e.getMessage());
}

Prevention

When it happens

Trigger: parseCustomConfigJson successfully parses the Struct's JSON but the result is not a java.util.Map — e.g. the custom config's Struct contains a single non-object value (string, number, list) instead of fields.

Common situations: Hand-written xDS test fixtures putting a bare value in custom_config; a control plane serializing the wrong field into custom config; user confusion between policy name and policy config (putting the name string where the config object belongs).

Related errors


AI-assisted analysis of grpc/grpc-java@64daddc1f3 (2026-09-08). Data as JSON: /api/errors/88a15dbadc459767. Report an issue: GitHub.