{"id":"e7598d264f02bdcc","repo":"google/gson","slug":"string-created-by-numberclass-is-not-a-val","errorCode":null,"errorMessage":"String created by \" + numberClass + \" is not a valid JSON number: \" + string","messagePattern":"String created by \" \\+ numberClass \\+ \" is not a valid JSON number: \" \\+ string","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"gson/src/main/java/com/google/gson/stream/JsonWriter.java","lineNumber":651,"sourceCode":"  public JsonWriter value(Number value) throws IOException {\n    if (value == null) {\n      return nullValue();\n    }\n\n    writeDeferredName();\n    String string = value.toString();\n    Class<? extends Number> numberClass = value.getClass();\n\n    if (!alwaysCreatesValidJsonNumber(numberClass)) {\n      // Validate that string is valid before writing it directly to JSON output\n      if (string.equals(\"-Infinity\") || string.equals(\"Infinity\") || string.equals(\"NaN\")) {\n        if (strictness != Strictness.LENIENT) {\n          throw new IllegalArgumentException(\"Numeric values must be finite, but was \" + string);\n        }\n      } else if (numberClass != Float.class\n          && numberClass != Double.class\n          && !VALID_JSON_NUMBER_PATTERN.matcher(string).matches()) {\n        throw new IllegalArgumentException(\n            \"String created by \" + numberClass + \" is not a valid JSON number: \" + string);\n      }\n    }\n\n    beforeValue();\n    out.append(string);\n    return this;\n  }\n\n  /**\n   * Encodes {@code null}.\n   *\n   * @return this writer.\n   */\n  @CanIgnoreReturnValue\n  public JsonWriter nullValue() throws IOException {\n    if (deferredName != null) {\n      if (serializeNulls) {","sourceCodeStart":633,"sourceCodeEnd":669,"githubUrl":"https://github.com/google/gson/blob/8b8628c65699bc4421696183c62ae0c1b9b281dc/gson/src/main/java/com/google/gson/stream/JsonWriter.java#L633-L669","documentation":"Thrown by JsonWriter.value(Number) when the argument is a Number subclass NOT in the trusted set (Integer, Long, Byte, Short, BigDecimal, BigInteger, AtomicInteger, AtomicLong) and NOT a Float/Double, and its toString() does not match Gson's VALID_JSON_NUMBER_PATTERN. Gson cannot assume an arbitrary Number's string form is JSON-safe, so it validates against the JSON number grammar and rejects anything malformed (e.g. leading zeros, hex, trailing junk, multiple dots). This protects downstream parsers from corrupt numeric tokens.","triggerScenarios":"Calling writer.value(new LazilyParsedNumber(\"0x1F\")), writer.value(customNumberSubclass) whose toString() returns \"1,000.0\" or \"1.2.3\" or \"$5\", or any third-party Number (e.g. a library-specific Decimal/Money type) whose toString deviates from JSON number syntax. Also fires if a TypeAdapter boxes a numeric string into a custom Number and forwards it.","commonSituations":"Using Joda/Joda-Money, JavaFX Point2D-style Number wrappers, Kotlin unsigned types bridged to java.lang.Number, or internal LazilyParsedNumber read from one JsonReader and re-serialized on another writer; bugs where toString() includes grouping separators or currency symbols from a Locale-aware format.","solutions":["Convert the custom Number to a trusted type before writing: writer.value(number.doubleValue()) or writer.value(new BigDecimal(number.toString())) after you have validated the string.","If you control the Number subclass, ensure toString() emits canonical JSON number syntax (optional sign, digits, single optional dot, optional exponent).","Sanitize/normalize the string with a regex and use writer.jsonValue(canonicalString) only after confirming it matches the JSON number grammar.","Strip locale formatting (grouping separators, currency symbols) before serialization."],"exampleFix":"// before: Money extends Number, toString() => \"$1,234.50\"\nwriter.value(money);\n\n// after\nwriter.value(money.getAmount()); // BigDecimal, a trusted type","handlingStrategy":"type-guard","validationCode":"// Validate a custom Number's toString against JSON number grammar before writing\nprivate static final java.util.regex.Pattern JSON_NUMBER =\n    java.util.regex.Pattern.compile(\"-?(?:0|[1-9][0-9]*)(?:\\\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\");\n\npublic static boolean isJsonNumberString(Number n) {\n  if (n == null) return false;\n  String s = n.toString();\n  return JSON_NUMBER.matcher(s).matches();\n}","typeGuard":"// True if Gson trusts the Number subclass to always produce valid JSON numbers\npublic static boolean isTrustedNumberType(Number n) {\n  if (n == null) return false;\n  Class<?> c = n.getClass();\n  return c == Integer.class || c == Long.class || c == Byte.class || c == Short.class\n      || c == BigDecimal.class || c == BigInteger.class\n      || c == AtomicInteger.class || c == AtomicLong.class;\n}","tryCatchPattern":"try {\n  writer.value(customNumber);\n} catch (IllegalArgumentException e) {\n  // fallback: emit as double, or as a string, or null\n  writer.value(customNumber.doubleValue());\n}","preventionTips":["Convert third-party/Money/Decimal Number types to BigDecimal or double at the boundary, not at serialization time.","If you implement a Number subclass, make toString() emit canonical JSON number syntax.","Avoid forwarding LazilyParsedNumber read from one reader onto another writer; convert first.","Strip Locale-aware grouping separators and currency symbols before relying on toString()."],"tags":["json","gson","serialization","numeric","custom-type","strictness"],"analyzedSha":"8b8628c65699bc4421696183c62ae0c1b9b281dc","analyzedAt":"2026-08-04T19:12:22.202Z","schemaVersion":2}