Writing¶
Glaze provides a unified return type for all write operations that gives you both error information and the byte count written.
The error_ctx Type¶
All write functions return glz::error_ctx:
struct error_ctx {
size_t count{}; // Bytes written to output
error_code ec{}; // Error code (none on success)
std::string_view custom_error_message{}; // Optional error details
operator bool() const noexcept; // Returns true when there IS an error
bool operator==(error_code e) const noexcept;
};
Key Properties¶
count: Number of bytes written, even on error (useful for debugging)ec: The error code (error_code::noneon success)operator bool(): Returnstruewhen there is an error (matchesstd::error_codesemantics)
Basic Usage¶
Writing to Resizable Buffers¶
my_struct obj{};
std::string buffer{};
auto ec = glz::write_json(obj, buffer);
if (ec) {
// Error occurred
std::cerr << glz::format_error(ec, buffer) << '\n';
return;
}
// Success: for resizable buffers, buffer.size() == ec.count
std::cout << "Wrote " << ec.count << " bytes\n";
Writing to Fixed-Size Buffers¶
Fixed-size buffers like std::array and std::span have automatic bounds checking:
std::array<char, 512> buffer; // Minimum 512 bytes recommended
auto ec = glz::write_json(obj, buffer);
if (ec) {
if (ec.ec == glz::error_code::buffer_overflow) {
// Buffer was too small
std::cerr << "Buffer overflow after " << ec.count << " bytes\n";
// Use a larger buffer or the string-returning overload:
auto result = glz::write_json(obj);
if (result) {
size_t required_size = result->size();
}
}
return;
}
// Success: ec.count contains bytes written
std::string_view json(buffer.data(), ec.count);
Writing to std::span¶
std::span is ideal for writing to external memory (shared memory, DMA buffers, memory-mapped files):
std::span<char> shared_memory(ptr, size);
auto ec = glz::write_json(obj, shared_memory);
if (ec) {
if (ec.ec == glz::error_code::buffer_overflow) {
return error::insufficient_shared_memory;
}
return error::serialization_failed;
}
// Notify consumers of bytes written
notify_consumers(ec.count);
Writing to Raw Pointers¶
Raw char* pointers are trusted to have sufficient space (no bounds checking):
char* dma_buffer = get_dma_buffer(); // Caller guarantees sufficient space
auto ec = glz::write_json(obj, dma_buffer);
if (!ec) {
trigger_dma_transfer(ec.count);
}
Writing to Streams¶
For writing directly to files or output streams with bounded memory, see Streaming I/O.
Buffer Overflow Handling¶
When writing to a fixed-size buffer that's too small, Glaze returns error_code::buffer_overflow:
std::array<char, 64> small_buffer;
auto ec = glz::write_json(large_object, small_buffer);
if (ec.ec == glz::error_code::buffer_overflow) {
// ec.count contains bytes written before overflow
// This is a LOWER BOUND on required size (not total required)
// The partial content is NOT valid JSON - do not parse or transmit
// Useful only for debugging:
std::string_view partial(small_buffer.data(), ec.count);
log_debug("Overflow after {} bytes: {}", ec.count, partial);
// To get actual required size, use the string-returning overload:
auto full = glz::write_json(large_object);
if (full) {
size_t required = full->size();
}
}
Important Notes on Buffer Overflow¶
- Content validity: The first
ec.countbytes contain valid serialized output - Partial data is NOT valid: The content is truncated mid-serialization and cannot be parsed
ec.countis a lower bound: It's the bytes written before failure, not total required size- Content beyond
ec.count: Unspecified (may be uninitialized or previous data)
Minimum Buffer Size Requirement¶
Bounded buffers (std::array, std::span, etc.) must be at least 512 bytes for reliable serialization. This requirement exists because Glaze uses internal padding for efficient writes. Buffers smaller than 512 bytes may return buffer_overflow even when the serialized output would fit.
// Good: 512+ bytes
std::array<char, 512> buffer;
auto ec = glz::write_json(obj, buffer);
// May fail unexpectedly for small outputs
std::array<char, 64> small_buffer; // Not recommended
For very small payloads where memory is extremely constrained, use a resizable buffer (std::string) and copy the result, or use raw char* if you can guarantee sufficient space.
Performance Optimization¶
Skipping Bounds Checking¶
For performance-critical paths where you've pre-validated buffer size:
struct fast_opts : glz::opts {
bool assume_sufficient_buffer = true;
};
std::array<char, 8192> large_buffer; // Known to be sufficient
auto ec = glz::write<fast_opts{}>(obj, large_buffer);
// No bounds checking overhead
Warning: Only use
assume_sufficient_bufferwhen you've verified the buffer is large enough. Buffer overflow with this option leads to undefined behavior.
Format Support¶
The error_ctx type is used consistently across all serialization formats:
// JSON
auto ec = glz::write_json(obj, buffer);
// BEVE (Binary)
auto ec = glz::write_beve(obj, buffer);
// CBOR
auto ec = glz::write_cbor(obj, buffer);
// MessagePack
auto ec = glz::write_msgpack(obj, buffer);
// Generic with options
auto ec = glz::write<glz::opts{.format = glz::JSON}>(obj, buffer);
Extending Buffer Support¶
Glaze uses a traits system that can be specialized for custom buffer types:
namespace glz {
template <size_t N>
struct buffer_traits<my_lib::ring_buffer<N>> {
static constexpr bool is_resizable = false;
static constexpr bool has_bounded_capacity = true;
static size_t capacity(const my_lib::ring_buffer<N>& b) noexcept {
return b.available_write_space();
}
static bool ensure_capacity(my_lib::ring_buffer<N>& b, size_t needed) noexcept {
return b.available_write_space() >= needed;
}
static void finalize(my_lib::ring_buffer<N>& b, size_t written) noexcept {
b.commit(written);
}
};
}
// Now works seamlessly
my_lib::ring_buffer<4096> ring;
auto ec = glz::write_json(obj, ring);
Growth policy (grow)¶
A buffer that can resize chooses how much it resizes by. Define grow to say so:
namespace glz {
template <>
struct buffer_traits<my_lib::arena_buffer> {
static constexpr bool is_resizable = true;
// ... capacity / ensure_capacity / finalize as above ...
// `required` is the logical end position the write needs to be able to address.
static void grow(my_lib::arena_buffer& b, size_t required) {
b.resize(required + my_lib::arena_buffer::block_size);
}
};
}
grow is optional. A specialization that omits it gets the default policy, resize(2 * required), which amortizes repeated reallocations to O(n) over a whole write.
The argument is a logical end position, not an amount of storage to add. For an ordinary buffer the two are the same thing, because logical position and physical offset coincide. They part ways for a buffer that does not keep the whole document — basic_ostream_buffer holds only the span it has not flushed yet, and its logical index keeps climbing after a flush releases the bytes behind it. Doubling that index would size storage to the document instead of to the window, so it defines its own grow that reserves the window the write needs plus a fixed slack. Define grow whenever your buffer's storage is not simply a prefix of the logical stream.
See Also¶
- Reading - Reading from buffers
- Streaming I/O - Reading/writing with streams
- Options - Compile time options including
assume_sufficient_buffer - Binary Format (BEVE) - Binary serialization