Skip to content

Fix staj_event::as_double silently returning 0 for non-numeric strings#711

Open
jsutes wants to merge 2 commits into
danielaparker:masterfrom
jsutes:fix-staj-event-as-double-error-propagation
Open

Fix staj_event::as_double silently returning 0 for non-numeric strings#711
jsutes wants to merge 2 commits into
danielaparker:masterfrom
jsutes:fix-staj-event-as-double-error-propagation

Conversation

@jsutes
Copy link
Copy Markdown

@jsutes jsutes commented May 22, 2026

Summary

basic_staj_event::as_double silently returns 0.0 (with no error) when a
string_value/key event holds text that isn't a number. Any decoder that
asks a string event for a double — e.g.
decode_csv<std::vector<std::vector<double>>> over infer_types(true) output —
therefore coerces non-numeric and empty cells to 0.0 instead of reporting a
conversion error.

Root cause

Regression introduced in caacd25 ("Remove chars_to, replace with to_double").

Before that commit, the string branch of as_double used the chars_to
functor, which threw json_runtime_error<std::invalid_argument> on invalid
input. The commit migrated to the new out-parameter API:

-                jsoncons::utility::chars_to f;
-                return f(value_.string_data_, length_);
+                double val{0};
+                jsoncons::utility::to_double(value_.string_data_, length_, val);
+                return val;

The new call reports failure through its return value, but this call site
discards it — so val keeps its default 0 and the caller's std::error_code&
is never set. (Later renames turned to_double into decstr_to_double; the
behavioral bug is unchanged.)

For comparison, basic_csv_parser::end_value_with_numeric_check uses the same
decstr_to_double call and does check result.ec.

Fix

include/jsoncons/staj_event.hpp, as_double:

double val{0};
auto result = jsoncons::decstr_to_double(value_.string_data_, length_, val);
if (!result)
{
    ec = conv_errc::not_double;
}
return val;

to_number_result has an explicit operator bool() that is false on error, so
!result covers both std::errc::invalid_argument (non-numeric / empty input)
and any other failure.

Reproducer

#include <jsoncons_ext/csv/csv.hpp>
#include <vector>
#include <string>

int main() {
    auto opt = jsoncons::csv::csv_options{}
        .mapping_kind(jsoncons::csv::csv_mapping_kind::n_rows)
        .assume_header(false);

    // Expected: an error. Actual (before this fix): silent {{0}}.
    auto r = jsoncons::csv::try_decode_csv<std::vector<std::vector<double>>>(
        std::string("hey\n"), opt);
    return r ? 1 : 0;   // returns 1 (success) before the fix
}

Tests

Added TEST_CASE("decode_csv non-numeric string into double reports error")
to test/csv/src/encode_decode_csv_tests.cpp, covering a non-numeric token
("hey\n") and an empty trailing field ("0,\n"). Each section asserts both
try_decode_csv returns an error and decode_csv throws.

  • The new test fails on current master (the try_decode_csv call silently
    succeeds).
  • With the fix, the new test passes and the full unit-test suite is green
    (883 test cases, 20,639 assertions).

jsutes added 2 commits May 21, 2026 22:03
Regression introduced in caacd25 ("Remove chars_to, replace with
to_double"): the migration replaced a throwing chars_to functor with the
new out-parameter to_double API but discarded its return code, leaving
the output at its default 0 with no error propagated to the caller's
std::error_code&.

As a result, decoding a non-numeric or empty CSV cell into a
floating-point type (e.g. decode_csv<std::vector<std::vector<double>>>)
silently produced 0.0 instead of reporting an error.

Check the result of decstr_to_double and set ec = conv_errc::not_double
on failure, matching how the same call is handled in
basic_csv_parser::end_value_with_numeric_check.
The strtod-family fallbacks of decstr_to_double (used when
JSONCONS_HAS_STD_FROM_CHARS is not defined, i.e. GCC < 11 and all Clang)
return success for zero-length input: their error check `if (end < cur)`
is unreachable when length == 0, because cur == s and end >= s always.

The previous commit therefore only fixed the empty-cell case on
platforms with std::from_chars. Guard length_ == 0 explicitly in
as_double so an empty string is reported as not_double on every
platform.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant