oracle/graal · error · IllegalArgumentException

%s option '%s' must have %s value, not %s [toString: %s]

Error message

%s option '%s' must have %s value, not %s [toString: %s]

What it means

Thrown by OptionsParser.parseOptionValue when the value object is not a String and its runtime class does not exactly equal the option's declared type. When values arrive as Strings they go through per-type parsing, but non-String values (used when options are set programmatically via an EconomicMap rather than parsed from text) must already be the exact boxed type (Boolean, Integer, Long, Float, Double, etc.).

Source

Thrown at compiler/src/jdk.graal.compiler.options/src/jdk/graal/compiler/options/OptionsParser.java:228

            }
            throw new IllegalArgumentException(msg.toString());
        }

        Object value = parseOptionValue(desc, uncheckedValue);

        desc.getOptionKey().update(values, value);
    }

    /**
     * Parses a given option value with a known descriptor.
     */
    public static Object parseOptionValue(OptionDescriptor desc, Object uncheckedValue) {
        Class<?> optionType = desc.getOptionValueType();
        Object value;
        if (!(uncheckedValue instanceof String valueString)) {
            if (optionType != uncheckedValue.getClass()) {
                String type = optionType.getSimpleName();
                throw new IllegalArgumentException(type + " option '" + desc.getName() + "' must have " + type + " value, not " + uncheckedValue.getClass() + " [toString: " + uncheckedValue + "]");
            }
            value = uncheckedValue;
        } else {
            if (optionType == Boolean.class) {
                if ("true".equals(valueString)) {
                    value = Boolean.TRUE;
                } else if ("false".equals(valueString)) {
                    value = Boolean.FALSE;
                } else {
                    throw new IllegalArgumentException("Boolean option '" + desc.getName() + "' must have value \"true\" or \"false\", not \"" + uncheckedValue + "\"");
                }
            } else if (optionType == String.class) {
                value = valueString;
            } else if (Enum.class.isAssignableFrom(optionType)) {
                value = ((EnumOptionKey<?>) desc.getOptionKey()).valueOf(valueString);
            } else if (optionType == EconomicSet.class) {
                value = ((EnumMultiOptionKey<?>) desc.getOptionKey()).valueOf(valueString);
            } else {

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Convert the value to the exact declared type shown in the message (e.g. ((Number)v).longValue() for a Long option).
  2. Prefer passing the value as a String and let parseOptionValue do the typed parsing (String branch handles Boolean/Enum/numbers).
  3. When loading config generically, coerce numeric types to the option type from desc.getOptionValueType().
  4. Check the '[toString: ...]' part of the message to confirm what object actually arrived.

Example fix

// before
OptionDescriptor d = ...; // option type Long
Object v = 42;           // Integer
Object parsed = OptionsParser.parseOptionValue(d, v);

// after
Object v = Long.valueOf(42);        // exact Long
// or simply pass the string form:
Object parsed = OptionsParser.parseOptionValue(d, "42");
Defensive patterns

Strategy: type-guard

Validate before calling

Object coerceToOptionType(OptionDescriptor desc, Object v) {
    Class<?> t = desc.getOptionValueType();
    if (v instanceof String s) return OptionsParser.parseOptionValue(desc, s);
    if (t == Long.class && v instanceof Number n) return n.longValue();
    if (t == Integer.class && v instanceof Number n) return n.intValue();
    if (t == Double.class && v instanceof Number n) return n.doubleValue();
    if (t == Float.class && v instanceof Number n) return n.floatValue();
    if (t == Boolean.class && v instanceof Boolean) return v;
    if (v != null && v.getClass() == t) return v;
    throw new IllegalArgumentException("Cannot coerce " + v + " to " + t.getSimpleName());
}

Type guard

boolean matchesOptionType(OptionDescriptor desc, Object v) {
    return v != null && v.getClass() == desc.getOptionValueType();
}

Try / catch

try {
    value = OptionsParser.parseOptionValue(desc, uncheckedValue);
} catch (IllegalArgumentException e) {
    if (e.getMessage().contains("must have") && e.getMessage().contains("[toString:")) {
        value = OptionsParser.parseOptionValue(desc, String.valueOf(uncheckedValue)); // re-parse from string form
    } else throw e;
}

Prevention

When it happens

Trigger: Calling parseOptionValue(desc, someObject) where someObject is, say, an Integer for a Long-typed option (Integer.class != Long.class), a primitive-converted Short, or an arbitrary object. Note the comparison is exact-class equality, not instanceof, so subclasses and wider/narrower numeric boxes all fail.

Common situations: Programmatically seeding option maps from untyped config (JSON/YAML numbers parsed as Integer when the option is Long); passing an enum's name String where an EnumOptionKey expects the Enum instance is fine (String branch), but passing a boxed Integer where Boolean is expected fails; reflective code pulling values from a Map<Object,Object>.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/d8a13ea4e8cde454. Report an issue: GitHub.