diff --git a/src/json.hpp b/src/json.hpp
index 0f452706..1094aade 100644
--- a/src/json.hpp
+++ b/src/json.hpp
@@ -8252,9 +8252,14 @@ class basic_json
     {
       public:
         template<typename NumberType>
-        numtostr(NumberType value, std::ostream& o)
+        numtostr(NumberType value)
         {
-            x_write(value, std::is_integral<NumberType>(), o);
+            x_write(value, std::is_integral<NumberType>());
+        }
+
+        const char* c_str() const
+        {
+            return m_buf.data();
         }
 
       private:
@@ -8262,12 +8267,12 @@ class basic_json
         std::array < char, 64 > m_buf{{}};
 
         template<typename NumberType>
-        void x_write(NumberType x, /*is_integral=*/std::true_type, std::ostream& o)
+        void x_write(NumberType x, /*is_integral=*/std::true_type)
         {
             // special case for "0"
             if (x == 0)
             {
-                o.put('0');
+                m_buf[0] = '0';
                 return;
             }
 
@@ -8293,31 +8298,30 @@ class basic_json
             }
 
             std::reverse(m_buf.begin(), m_buf.begin() + i);
-            o.write(m_buf.data(), static_cast<std::streamsize>(i));
         }
 
         template<typename NumberType>
-        void x_write(NumberType x, /*is_integral=*/std::false_type, std::ostream& o)
+        void x_write(NumberType x, /*is_integral=*/std::false_type)
         {
             // special case for 0.0 and -0.0
             if (x == 0)
             {
+                size_t i = 0;
                 if (std::signbit(x))
                 {
-                    o.write("-0.0", 4);
-                }
-                else
-                {
-                    o.write("0.0", 3);
+                    m_buf[i++] = '-';
                 }
+                m_buf[i++] = '0';
+                m_buf[i++] = '.';
+                m_buf[i] = '0';
                 return;
             }
 
             // get number of digits for a text -> float -> text round-trip
             static constexpr auto d = std::numeric_limits<NumberType>::digits10;
 
-            // the actual conversion; store how many bytes have been written
-            auto written_bytes = snprintf(m_buf.data(), m_buf.size(), "%.*g", d, x);
+            // the actual conversion
+            const auto written_bytes = snprintf(m_buf.data(), m_buf.size(), "%.*g", d, x);
 
             // negative value indicates an error
             assert(written_bytes > 0);
@@ -8338,7 +8342,6 @@ class basic_json
             {
                 const auto end = std::remove(m_buf.begin(), m_buf.begin() + written_bytes, thousands_sep);
                 std::fill(end, m_buf.end(), '\0');
-                written_bytes -= m_buf.end() - end;
             }
 
             // convert decimal point to '.'
@@ -8354,19 +8357,36 @@ class basic_json
                 }
             }
 
-            o.write(m_buf.data(), static_cast<std::streamsize>(written_bytes));
-
             // determine if need to append ".0"
-            const bool value_is_int_like = std::all_of(m_buf.begin(),
-                                           m_buf.begin() + written_bytes + 1,
-                                           [](char c)
+            size_t i = 0;
+            bool value_is_int_like = true;
+            for (i = 0; i < m_buf.size(); ++i)
             {
-                // we use %g above, so there cannot be an 'E' character
-                return c != '.' and c != 'e';
-            });
+                // break when end of number is reached
+                if (m_buf[i] == '\0')
+                {
+                    break;
+                }
+
+                // check if we find non-int character
+                value_is_int_like = value_is_int_like and m_buf[i] != '.' and
+                                    m_buf[i] != 'e' and m_buf[i] != 'E';
+            }
+
             if (value_is_int_like)
             {
-                o.write(".0", 2);
+                // there must be 2 bytes left for ".0"
+                assert((i + 2) < m_buf.size());
+                // we write to the end of the number
+                assert(m_buf[i] == '\0');
+                assert(m_buf[i - 1] != '\0');
+
+                // add ".0"
+                m_buf[i] = '.';
+                m_buf[i + 1] = '0';
+
+                // the resulting string is properly terminated
+                assert(m_buf[i + 2] == '\0');
             }
         }
     };
@@ -8546,19 +8566,19 @@ class basic_json
 
             case value_t::number_integer:
             {
-                numtostr(m_value.number_integer, o);
+                o << numtostr(m_value.number_integer).c_str();
                 return;
             }
 
             case value_t::number_unsigned:
             {
-                numtostr(m_value.number_unsigned, o);
+                o << numtostr(m_value.number_unsigned).c_str();
                 return;
             }
 
             case value_t::number_float:
             {
-                numtostr(m_value.number_float, o);
+                o << numtostr(m_value.number_float).c_str();
                 return;
             }
 
diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c
index b4b9e3d2..a686a1a7 100644
--- a/src/json.hpp.re2c
+++ b/src/json.hpp.re2c
@@ -8252,9 +8252,14 @@ class basic_json
     {
       public:
         template<typename NumberType>
-        numtostr(NumberType value, std::ostream& o)
+        numtostr(NumberType value)
         {
-            x_write(value, std::is_integral<NumberType>(), o);
+            x_write(value, std::is_integral<NumberType>());
+        }
+
+        const char* c_str() const
+        {
+            return m_buf.data();
         }
 
       private:
@@ -8262,12 +8267,12 @@ class basic_json
         std::array < char, 64 > m_buf{{}};
 
         template<typename NumberType>
-        void x_write(NumberType x, /*is_integral=*/std::true_type, std::ostream& o)
+        void x_write(NumberType x, /*is_integral=*/std::true_type)
         {
             // special case for "0"
             if (x == 0)
             {
-                o.put('0');
+                m_buf[0] = '0';
                 return;
             }
 
@@ -8293,31 +8298,30 @@ class basic_json
             }
 
             std::reverse(m_buf.begin(), m_buf.begin() + i);
-            o.write(m_buf.data(), static_cast<std::streamsize>(i));
         }
 
         template<typename NumberType>
-        void x_write(NumberType x, /*is_integral=*/std::false_type, std::ostream& o)
+        void x_write(NumberType x, /*is_integral=*/std::false_type)
         {
             // special case for 0.0 and -0.0
             if (x == 0)
             {
+                size_t i = 0;
                 if (std::signbit(x))
                 {
-                    o.write("-0.0", 4);
-                }
-                else
-                {
-                    o.write("0.0", 3);
+                    m_buf[i++] = '-';
                 }
+                m_buf[i++] = '0';
+                m_buf[i++] = '.';
+                m_buf[i] = '0';
                 return;
             }
 
             // get number of digits for a text -> float -> text round-trip
             static constexpr auto d = std::numeric_limits<NumberType>::digits10;
 
-            // the actual conversion; store how many bytes have been written
-            auto written_bytes = snprintf(m_buf.data(), m_buf.size(), "%.*g", d, x);
+            // the actual conversion
+            const auto written_bytes = snprintf(m_buf.data(), m_buf.size(), "%.*g", d, x);
 
             // negative value indicates an error
             assert(written_bytes > 0);
@@ -8338,7 +8342,6 @@ class basic_json
             {
                 const auto end = std::remove(m_buf.begin(), m_buf.begin() + written_bytes, thousands_sep);
                 std::fill(end, m_buf.end(), '\0');
-                written_bytes -= m_buf.end() - end;
             }
 
             // convert decimal point to '.'
@@ -8354,19 +8357,36 @@ class basic_json
                 }
             }
 
-            o.write(m_buf.data(), static_cast<std::streamsize>(written_bytes));
-
             // determine if need to append ".0"
-            const bool value_is_int_like = std::all_of(m_buf.begin(),
-                                           m_buf.begin() + written_bytes + 1,
-                                           [](char c)
+            size_t i = 0;
+            bool value_is_int_like = true;
+            for (i = 0; i < m_buf.size(); ++i)
             {
-                // we use %g above, so there cannot be an 'E' character
-                return c != '.' and c != 'e';
-            });
+                // break when end of number is reached
+                if (m_buf[i] == '\0')
+                {
+                    break;
+                }
+
+                // check if we find non-int character
+                value_is_int_like = value_is_int_like and m_buf[i] != '.' and
+                                    m_buf[i] != 'e' and m_buf[i] != 'E';
+            }
+
             if (value_is_int_like)
             {
-                o.write(".0", 2);
+                // there must be 2 bytes left for ".0"
+                assert((i + 2) < m_buf.size());
+                // we write to the end of the number
+                assert(m_buf[i] == '\0');
+                assert(m_buf[i - 1] != '\0');
+
+                // add ".0"
+                m_buf[i] = '.';
+                m_buf[i + 1] = '0';
+
+                // the resulting string is properly terminated
+                assert(m_buf[i + 2] == '\0');
             }
         }
     };
@@ -8546,19 +8566,19 @@ class basic_json
 
             case value_t::number_integer:
             {
-                numtostr(m_value.number_integer, o);
+                o << numtostr(m_value.number_integer).c_str();
                 return;
             }
 
             case value_t::number_unsigned:
             {
-                numtostr(m_value.number_unsigned, o);
+                o << numtostr(m_value.number_unsigned).c_str();
                 return;
             }
 
             case value_t::number_float:
             {
-                numtostr(m_value.number_float, o);
+                o << numtostr(m_value.number_float).c_str();
                 return;
             }