-
Notifications
You must be signed in to change notification settings - Fork 617
fix: make numeric boolean coercion consistent #7804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3868,6 +3868,37 @@ public Boolean readBool() { | |||||||||||||||||||||||||||||||||||||||||||||||||
| return boolValue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| protected final boolean getBooleanValue() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return (context.features & Feature.NonZeroNumberCastToBooleanAsTrue.mask) != 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? isNonZeroNumber() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : isOneNumber(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private boolean isNonZeroNumber() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return mag0 != 0 || mag1 != 0 || mag2 != 0 || mag3 != 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private boolean isOneNumber() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (valueType == JSON_TYPE_INT) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return isOneInt(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return isOneDecimal(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private boolean isOneInt() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return mag0 == 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| && mag1 == 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| && mag2 == 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| && mag3 == 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private boolean isOneDecimal() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return getBigDecimal() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .abs() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .compareTo(BigDecimal.ONE) == 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3897
to
+3899
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Every non-INT number in a boolean position now pays one extra Measured with a The exact ±1 test is computable allocation-free from — qwen3.8-max via Qwen Code /review (v0.22.0) |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3896
to
+3900
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] Two shapes, both reproduced on the UTF-16 and UTF-8 readers:
Witness (probe on this PR's compiled code): Root-cause fix: in
Suggested change
If you apply either fix, please add — qwen3.8-max via Qwen Code /review (v0.22.0) |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Reads a boolean value from JSON data as a primitive boolean. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package com.alibaba.fastjson2; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
|
|
||
| public class JSONReaderBooleanTest { | ||
| @Test | ||
| public void numberNotation() { | ||
| assertValue(true, "1"); | ||
| assertValue(true, "1.0"); | ||
| assertValue(true, "1.00"); | ||
| assertValue(true, "1e0"); | ||
| assertValue(true, "10e-1"); | ||
| assertValue(true, "-1"); | ||
| assertValue(true, "-1.0"); | ||
| assertValue(false, "2"); | ||
| assertValue(false, "2.0"); | ||
| assertValue(false, "-2"); | ||
| assertValue(false, "-2.0"); | ||
| assertValue(false, "0"); | ||
| assertValue(false, "0.0"); | ||
| assertValue(false, "0.5"); | ||
| } | ||
|
|
||
| @Test | ||
| public void nonZeroNumberFeature() { | ||
| for (String value : new String[]{"1", "1.0", "-1", "-1.0", "2", "2.0", "0.5", "-0.5"}) { | ||
| assertFeatureValue(true, value); | ||
| } | ||
|
|
||
| for (String value : new String[]{"0", "0.0", "0e10"}) { | ||
| assertFeatureValue(false, value); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void stringAndNullBehavior() { | ||
| assertValue(true, "\"1\""); | ||
| assertValue(true, "\"true\""); | ||
| assertValue(false, "null"); | ||
| assertThrows(JSONException.class, () -> parse("\"yes\"")); | ||
| } | ||
|
|
||
| private static void assertValue(boolean expected, String value) { | ||
| String json = "{\"enabled\":" + value + "}"; | ||
| assertEquals(expected, JSON.parseObject(json, Bean.class).enabled, value); | ||
| assertEquals(expected, JSON.parseObject(json.toCharArray(), Bean.class).enabled, value); | ||
| assertEquals( | ||
| expected, | ||
| JSON.parseObject(json.getBytes(StandardCharsets.UTF_8), Bean.class).enabled, | ||
| value | ||
| ); | ||
| } | ||
|
|
||
| private static boolean parse(String value) { | ||
| return JSON.parseObject("{\"enabled\":" + value + "}", Bean.class).enabled; | ||
| } | ||
|
|
||
| private static void assertFeatureValue(boolean expected, String value) { | ||
| String json = "{\"enabled\":" + value + "}"; | ||
| assertEquals( | ||
| expected, | ||
| JSON.parseObject( | ||
| json, | ||
| Bean.class, | ||
| JSONReader.Feature.NonZeroNumberCastToBooleanAsTrue | ||
| ).enabled, | ||
| value | ||
| ); | ||
| assertEquals( | ||
| expected, | ||
| JSON.parseObject( | ||
| json.toCharArray(), | ||
| Bean.class, | ||
| JSONReader.Feature.NonZeroNumberCastToBooleanAsTrue | ||
| ).enabled, | ||
| value | ||
| ); | ||
| assertEquals( | ||
| expected, | ||
| JSON.parseObject( | ||
| json.getBytes(StandardCharsets.UTF_8), | ||
| Bean.class, | ||
| JSONReader.Feature.NonZeroNumberCastToBooleanAsTrue | ||
| ).enabled, | ||
| value | ||
| ); | ||
| } | ||
|
|
||
| public static class Bean { | ||
| public boolean enabled; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Critical] Type-suffixed number literals (
1B,1S,1L,1F,1D) in a boolean position now throwJSONException("TODO : N")instead of coercing to a boolean.readNumber0()unconditionally assignsJSON_TYPE_INT8/INT16/INT64/FLOAT/DOUBLEfor suffixed literals in both readers, butisOneNumber()only special-casesJSON_TYPE_INT, so the suffixed types fall intoisOneDecimal()→getBigDecimal(), whose switch has no case for them and hitsdefault: throw new JSONException("TODO : " + valueType). Before this PR the removed fall-through returnedfalsefor these inputs, so previously-parseable lenient input now fails to parse — including fastjson2's own writer output: withWriteClassName,JSON.toJSONStringemits1L, and re-parsing that into a boolean field throws. The behavior is also self-inconsistent: withNonZeroNumberCastToBooleanAsTrueenabled, the same1Lreturnstruevia the mag path instead of throwing.Witness (probe on this PR's compiled code; String, char[] and byte[] inputs all identical):
The integer-family suffixed types share the mag layout, so
isOneInt()works for them as-is (1L→true, matching the feature path);FLOAT/DOUBLEkeep the pre-PR no-throw fallback. If you apply this, please add cases likeassertValue(true, "1L")/assertValue(false, "1F")toJSONReaderBooleanTest.numberNotation()and confirm they fail withJSONExceptionif the old dispatch is restored.— qwen3.8-max via Qwen Code /review (v0.22.0)