Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add JsonNumericMode parameter to jsonDecode for opt-in all-double par…
…sing

JSON (ECMA-404) does not distinguish between integer and floating-point
numbers — both are simply "number". Dart's jsonDecode currently returns
int for numbers without a decimal point and double otherwise, which
causes TypeError when consuming APIs that may return either form for
the same field.

This adds an optional numericMode parameter to jsonDecode, JsonCodec.decode,
and JsonDecoder that controls how numbers are parsed:

- JsonNumericMode.preserveType (default): current behavior, no changes
- JsonNumericMode.allDouble: all JSON numbers are parsed as double

The implementation converts int to double at the listener level in each
platform backend (VM, JS runtime, WASM), keeping the performance-critical
parseNumber logic untouched.

Closes #62776
See also #46883, #55499
  • Loading branch information
fabricio-costa authored and f-b-costa committed Feb 26, 2026
commit a91a3234f7d9fdee1f453c5f940f10dffdd49ac2
29 changes: 23 additions & 6 deletions sdk/lib/_internal/js_runtime/lib/convert_patch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import 'dart:collection' show LinkedHashMap, MapBase;
///
/// Throws [FormatException] if the input is not valid JSON text.
@patch
_parseJson(String source, reviver(key, value)?) {
_parseJson(String source, reviver(key, value)?, JsonNumericMode numericMode) {
if (source is! String) throw argumentErrorValue(source);

var parsed;
Expand All @@ -39,10 +39,25 @@ _parseJson(String source, reviver(key, value)?) {
throw FormatException(JS<String>('String', 'String(#)', e));
}

if (reviver == null) {
bool allDouble = numericMode == JsonNumericMode.allDouble;

if (reviver == null && !allDouble) {
return _convertJsonToDartLazy(parsed);
} else if (reviver == null && allDouble) {
// Use a synthetic reviver that converts ints to doubles.
return _convertJsonToDart(parsed, (key, value) {
if (value is int) return value.toDouble();
return value;
});
} else if (allDouble) {
// Wrap user reviver to also convert ints to doubles.
final userReviver = reviver!;
return _convertJsonToDart(parsed, (key, value) {
if (value is int) value = value.toDouble();
return userReviver(key, value);
});
} else {
return _convertJsonToDart(parsed, reviver);
return _convertJsonToDart(parsed, reviver!);
}
}

Expand Down Expand Up @@ -357,7 +372,7 @@ class _JsonMapKeyIterable extends ListIterable<String> {
class JsonDecoder {
@patch
StringConversionSink startChunkedConversion(Sink<Object?> sink) {
return _JsonDecoderSink(_reviver, sink);
return _JsonDecoderSink(_reviver, sink, _numericMode);
}
}

Expand All @@ -369,14 +384,16 @@ class JsonDecoder {
class _JsonDecoderSink extends _StringSinkConversionSink<StringBuffer> {
final Object? Function(Object? key, Object? value)? _reviver;
final Sink<Object?> _sink;
final JsonNumericMode _numericMode;

_JsonDecoderSink(this._reviver, this._sink) : super(StringBuffer(''));
_JsonDecoderSink(this._reviver, this._sink, this._numericMode)
: super(StringBuffer(''));

void close() {
super.close();
String accumulated = _stringSink.toString();
_stringSink.clear();
Object? decoded = _parseJson(accumulated, _reviver);
Object? decoded = _parseJson(accumulated, _reviver, _numericMode);
_sink.add(decoded);
_sink.close();
}
Expand Down
51 changes: 35 additions & 16 deletions sdk/lib/_internal/vm/lib/convert_patch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import "dart:typed_data" show Uint8List, Uint16List;
dynamic _parseJson(
String source,
Object? Function(Object? key, Object? value)? reviver,
JsonNumericMode numericMode,
) {
_JsonListener listener = _JsonListener(reviver);
bool allDouble = numericMode == JsonNumericMode.allDouble;
_JsonListener listener = _JsonListener(reviver, allDouble);
var parser = _JsonStringParser(listener);
parser.chunk = source;
parser.chunkEnd = source.length;
Expand All @@ -45,6 +47,7 @@ class Utf8Decoder {
return _JsonUtf8Decoder(
(next as JsonDecoder)._reviver,
this._allowMalformed,
(next as JsonDecoder)._numericMode == JsonNumericMode.allDouble,
)
as dynamic /*=Converter<List<int>, T>*/;
}
Expand All @@ -55,18 +58,21 @@ class Utf8Decoder {
class _JsonUtf8Decoder extends Converter<List<int>, Object?> {
final Object? Function(Object? key, Object? value)? _reviver;
final bool _allowMalformed;
final bool _allDouble;

_JsonUtf8Decoder(this._reviver, this._allowMalformed);
_JsonUtf8Decoder(this._reviver, this._allowMalformed, [this._allDouble = false]);

Object? convert(List<int> input) {
var parser = _JsonUtf8DecoderSink._createParser(_reviver, _allowMalformed);
var parser = _JsonUtf8DecoderSink._createParser(
_reviver, _allowMalformed, _allDouble,
);
parser.parseChunk(input, 0, input.length);
parser.close();
return parser.result;
}

ByteConversionSink startChunkedConversion(Sink<Object?> sink) {
return _JsonUtf8DecoderSink(_reviver, sink, _allowMalformed);
return _JsonUtf8DecoderSink(_reviver, sink, _allowMalformed, _allDouble);
}
}

Expand All @@ -81,9 +87,10 @@ class _JsonUtf8Decoder extends Converter<List<int>, Object?> {
* seen value in a variable, and uses it depending on the following event.
*/
class _JsonListener {
_JsonListener(this.reviver);
_JsonListener(this.reviver, [this.allDouble = false]);

final Object? Function(Object? key, Object? value)? reviver;
final bool allDouble;

/**
* Stack used to handle nested containers.
Expand Down Expand Up @@ -120,7 +127,11 @@ class _JsonListener {
}

void handleNumber(num value) {
this.value = value;
if (allDouble && value is int) {
this.value = value.toDouble();
} else {
this.value = value;
}
}

void handleBool(bool value) {
Expand Down Expand Up @@ -1525,7 +1536,8 @@ class _JsonStringParser extends _JsonParserWithListener
class JsonDecoder {
@patch
StringConversionSink startChunkedConversion(Sink<Object?> sink) {
return _JsonStringDecoderSink(this._reviver, sink);
bool allDouble = this._numericMode == JsonNumericMode.allDouble;
return _JsonStringDecoderSink(this._reviver, sink, allDouble);
}
}

Expand All @@ -1539,14 +1551,16 @@ class _JsonStringDecoderSink extends StringConversionSinkBase {
_JsonStringParser _parser;
final Object? Function(Object? key, Object? value)? _reviver;
final Sink<Object?> _sink;
final bool _allDouble;

_JsonStringDecoderSink(this._reviver, this._sink)
: _parser = _createParser(_reviver);
_JsonStringDecoderSink(this._reviver, this._sink, this._allDouble)
: _parser = _createParser(_reviver, _allDouble);

static _JsonStringParser _createParser(
Object? Function(Object? key, Object? value)? reviver,
bool allDouble,
) {
return _JsonStringParser(_JsonListener(reviver));
return _JsonStringParser(_JsonListener(reviver, allDouble));
}

void addSlice(String chunk, int start, int end, bool isLast) {
Expand All @@ -1568,7 +1582,7 @@ class _JsonStringDecoderSink extends StringConversionSinkBase {
}

ByteConversionSink asUtf8Sink(bool allowMalformed) {
return _JsonUtf8DecoderSink(_reviver, _sink, allowMalformed);
return _JsonUtf8DecoderSink(_reviver, _sink, allowMalformed, _allDouble);
}
}

Expand Down Expand Up @@ -1669,14 +1683,19 @@ class _JsonUtf8DecoderSink extends ByteConversionSink {
final _JsonUtf8Parser _parser;
final Sink<Object?> _sink;

_JsonUtf8DecoderSink(reviver, this._sink, bool allowMalformed)
: _parser = _createParser(reviver, allowMalformed);
_JsonUtf8DecoderSink(
reviver,
this._sink,
bool allowMalformed, [
bool allDouble = false,
]) : _parser = _createParser(reviver, allowMalformed, allDouble);

static _JsonUtf8Parser _createParser(
Object? Function(Object? key, Object? value)? reviver,
bool allowMalformed,
) {
return _JsonUtf8Parser(_JsonListener(reviver), allowMalformed);
bool allowMalformed, [
bool allDouble = false,
]) {
return _JsonUtf8Parser(_JsonListener(reviver, allDouble), allowMalformed);
}

void addSlice(List<int> chunk, int start, int end, bool isLast) {
Expand Down
54 changes: 39 additions & 15 deletions sdk/lib/_internal/wasm/lib/convert_patch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import "dart:typed_data" show Uint8List;
dynamic _parseJson(
String source,
Object? Function(Object? key, Object? value)? reviver,
JsonNumericMode numericMode,
) {
final listener = _JsonListener(reviver);
bool allDouble = numericMode == JsonNumericMode.allDouble;
final listener = _JsonListener(reviver, allDouble);
final parser = _JSStringImplParser(listener);
parser.setNewChunk(unsafeCast<JSStringImpl>(source), source.length);
parser.parse(0);
Expand All @@ -41,6 +43,7 @@ class Utf8Decoder {
return _JsonUtf8Decoder(
(next as JsonDecoder)._reviver,
this._allowMalformed,
(next as JsonDecoder)._numericMode == JsonNumericMode.allDouble,
)
as dynamic /*=Converter<List<int>, T>*/;
}
Expand All @@ -51,18 +54,21 @@ class Utf8Decoder {
class _JsonUtf8Decoder extends Converter<List<int>, Object?> {
final Object? Function(Object? key, Object? value)? _reviver;
final bool _allowMalformed;
final bool _allDouble;

_JsonUtf8Decoder(this._reviver, this._allowMalformed);
_JsonUtf8Decoder(this._reviver, this._allowMalformed, [this._allDouble = false]);

Object? convert(List<int> input) {
var parser = _JsonUtf8DecoderSink._createParser(_reviver, _allowMalformed);
var parser = _JsonUtf8DecoderSink._createParser(
_reviver, _allowMalformed, _allDouble,
);
parser.parseChunk(input, 0, input.length);
parser.close();
return parser.result;
}

ByteConversionSink startChunkedConversion(Sink<Object?> sink) =>
_JsonUtf8DecoderSink(_reviver, sink, _allowMalformed);
_JsonUtf8DecoderSink(_reviver, sink, _allowMalformed, _allDouble);
}

//// Implementation ///////////////////////////////////////////////////////////
Expand All @@ -76,9 +82,10 @@ class _JsonUtf8Decoder extends Converter<List<int>, Object?> {
* seen value in a variable, and uses it depending on the following event.
*/
class _JsonListener {
_JsonListener(this.reviver);
_JsonListener(this.reviver, [this.allDouble = false]);

final Object? Function(Object? key, Object? value)? reviver;
final bool allDouble;

/**
* Stack used to handle nested containers.
Expand Down Expand Up @@ -159,6 +166,10 @@ class _JsonListener {

@pragma('wasm:prefer-inline')
void handleIntegerNumber(int value) {
if (allDouble) {
handleNumber(value.toDouble());
return;
}
if (value.toWasmI64().leU(0xff.toWasmI64())) {
handleNumber(_intBoxes256[value]);
return;
Expand All @@ -167,7 +178,11 @@ class _JsonListener {
}

void handleNumber(num value) {
this.value = value;
if (allDouble && value is int) {
this.value = value.toDouble();
} else {
this.value = value;
}
}

void handleBool(bool value) {
Expand Down Expand Up @@ -1858,8 +1873,10 @@ JSStringImpl _internJSStringFromAsciiSlice(
@patch
class JsonDecoder {
@patch
StringConversionSink startChunkedConversion(Sink<Object?> sink) =>
_JsonStringDecoderSink(this._reviver, sink);
StringConversionSink startChunkedConversion(Sink<Object?> sink) {
bool allDouble = this._numericMode == JsonNumericMode.allDouble;
return _JsonStringDecoderSink(this._reviver, sink, allDouble);
}
}

/**
Expand All @@ -1877,8 +1894,10 @@ class _JsonStringDecoderSink extends StringConversionSinkBase {

final Sink<Object?> _sink;

_JsonStringDecoderSink(this._reviver, this._sink)
: _listener = _JsonListener(_reviver) {
final bool _allDouble;

_JsonStringDecoderSink(this._reviver, this._sink, this._allDouble)
: _listener = _JsonListener(_reviver, _allDouble) {
_stringParser = _JSStringImplParser(_listener);
}

Expand All @@ -1900,7 +1919,7 @@ class _JsonStringDecoderSink extends StringConversionSinkBase {
}

ByteConversionSink asUtf8Sink(bool allowMalformed) =>
_JsonUtf8DecoderSink(_reviver, _sink, allowMalformed);
_JsonUtf8DecoderSink(_reviver, _sink, allowMalformed, _allDouble);
}

/**
Expand Down Expand Up @@ -2073,13 +2092,18 @@ class _JsonUtf8DecoderSink extends ByteConversionSink {
final _JsonUtf8Parser _parser;
final Sink<Object?> _sink;

_JsonUtf8DecoderSink(reviver, this._sink, bool allowMalformed)
: _parser = _createParser(reviver, allowMalformed);
_JsonUtf8DecoderSink(
reviver,
this._sink,
bool allowMalformed, [
bool allDouble = false,
]) : _parser = _createParser(reviver, allowMalformed, allDouble);

static _JsonUtf8Parser _createParser(
Object? Function(Object? key, Object? value)? reviver,
bool allowMalformed,
) => _JsonUtf8Parser(_JsonListener(reviver), allowMalformed);
bool allowMalformed, [
bool allDouble = false,
]) => _JsonUtf8Parser(_JsonListener(reviver, allDouble), allowMalformed);

void addSlice(List<int> chunk, int start, int end, bool isLast) {
_addChunk(chunk, start, end);
Expand Down
Loading