Skip to content

# Lazy BEVE Parsing

Glaze provides a truly lazy BEVE parser (`glz::lazy_beve`) that offers on-demand parsing without any upfront processing. This approach is ideal when you need to extract a few separate fields from large BEVE documents.

## When to Use Lazy BEVE

Use Case | Recommended Approach
--- | ---
Extract 1-3 fields from large BEVE | `glz::lazy_beve`
Access fields near the beginning | `glz::lazy_beve`
Full deserialization into structs | `glz::read_beve`
Iterate all elements (single pass) | `glz::lazy_beve`
Multiple random accesses to array | `glz::lazy_beve` with `.index()`
Unknown/dynamic BEVE structure with persistent memory | `glz::generic`

## Basic Usage

    #include "glaze/beve.hpp"

    struct User {
       std::string name{};
       int age{};
       bool active{};
    };

    User original{"John", 30, true};
    std::vector<std::byte> buffer;
    glz::write_beve(original, buffer);

    auto result = glz::lazy_beve(buffer);
    if (result) {
        auto& doc = *result;

        // Access fields lazily - only parses what you access
        auto name = doc["name"].get<std::string_view>();
        auto age = doc["age"].get<int64_t>();
        auto active = doc["active"].get<bool>();

        if (name && age && active) {
            std::cout << *name << " is " << *age << " years old\n";
        }
    }

## Why Lazy?

`glz::lazy_beve` does zero upfront work:

- `lazy_beve()` just stores a pointer and validates the first byte - O(1)
- Field access scans only the bytes needed to find that field

## Advantages of BEVE for Lazy Parsing

BEVE's binary format provides several advantages for lazy parsing compared to JSON:

Feature | BEVE | JSON
--- | --- | ---
Type detection | O(1) - single byte check | O(k) - scan to identify type
String length | O(1) - length-prefixed | O(n) - scan for closing quote
Container size | O(1) - count-prefixed | O(n) - scan all elements
Skip value | O(1) for fixed types | O(n) - must parse structure

## Nested Object Access

Access deeply nested fields efficiently:

    struct Address {
       std::string city{};
       std::string country{};
    };

    struct Profile {
       std::string name{};
       std::string email{};
    };

    struct UserProfile {
       Profile profile{};
       Address address{};
    };

    struct Container {
       UserProfile user{};
    };

    Container original{
       {{"Alice", "alice@example.com"}, {"New York", "USA"}}
    };

    std::vector<std::byte> buffer;
    glz::write_beve(original, buffer);

    auto result = glz::lazy_beve(buffer);
    if (result) {
        auto& doc = *result;

        // Chain field access - each level is lazy
        auto email = doc["user"]["profile"]["email"].get<std::string_view>();
        if (email) {
            std::cout << "Email: " << *email << "\n";
        }
    }

## Array Access

Access array elements by index:

    struct Item {
       int id{};
       int value{};
    };

    struct Container {
       std::vector<Item> items;
    };

    Container original{{{1, 100}, {2, 200}, {3, 300}}};
    std::vector<std::byte> buffer;
    glz::write_beve(original, buffer);

    auto result = glz::lazy_beve(buffer);
    if (result) {
        auto& doc = *result;

        // Access specific array element
        auto first_value = doc["items"][0]["value"].get<int64_t>();
        auto third_id = doc["items"][2]["id"].get<int64_t>();

        if (first_value && third_id) {
            std::cout << "First value: " << *first_value << "\n";
            std::cout << "Third id: " << *third_id << "\n";
        }
    }

## Iteration

Iterate over arrays and objects efficiently:

    std::map<std::string, int> data{{"a", 1}, {"b", 2}, {"c", 3}};
    std::vector<std::byte> buffer;
    glz::write_beve(data, buffer);

    auto result = glz::lazy_beve(buffer);
    if (result) {
        // Iterate object entries
        int64_t sum = 0;
        for (auto& item : result->root()) {
            auto val = item.get<int64_t>();
            if (val) sum += *val;
        }
        std::cout << "Sum: " << sum << "\n";  // Sum: 6
    }

For objects, you can access both keys and values:

    for (auto& item : result->root()) {
        std::cout << item.key() << ": ";
        auto val = item.get<int64_t>();
        if (val) std::cout << *val;
        std::cout << "\n";
    }

## Indexed Views for O(1) Access

For scenarios requiring multiple random accesses or repeated iteration, you can build an index for O(1) element access:

    std::map<std::string, int> data;
    for (int i = 0; i < 1000; ++i) {
        data["key" + std::to_string(i)] = i;
    }
    std::vector<std::byte> buffer;
    glz::write_beve(data, buffer);

    auto result = glz::lazy_beve(buffer);
    if (result) {
        // Build index once - O(n) scan
        auto indexed = result->root().index();

        // Now enjoy O(1) operations:
        size_t count = indexed.size();      // O(1) - no scanning
        auto key500 = indexed["key500"];    // Linear search in indexed keys
        auto first = indexed[0];            // O(1) - direct access by position

        // O(1) iteration advancement
        for (auto& item : indexed) {
            auto val = item.get<int64_t>();  // Value access still lazy
        }
    }

### When to Use `.index()`

Scenario | Without Index | With Index | Recommendation
--- | --- | --- | ---
Single random access | O(k) | O(n) build + O(1) | Don't index
5+ random accesses | O(5k) | O(n) build + O(5) | Use index
Multiple iterations | O(n) each | O(n) build + O(n) each | Use index
Need size before iterating | O(n) | O(1) after build | Use index
Single sequential iteration | O(n) | O(n) build + O(n) | Don't index

### Indexed View API

    auto indexed = doc["items"].index();

    // O(1) size query
    size_t count = indexed.size();

    // O(1) empty check
    if (!indexed.empty()) { /* ... */ }

    // O(1) random access by position
    auto third = indexed[2];

    // For indexed objects: O(n) key lookup (linear search)
    auto value = indexed["key"];

    // Check if object contains key
    if (indexed.contains("key")) { /* ... */ }

    // Full random-access iterator support
    auto it = indexed.begin();
    it += 50;                    // Jump forward 50 elements
    auto elem = it[10];          // Access 10 elements ahead
    auto dist = indexed.end() - it;  // Distance to end

## Optimizing Performance: Sequential Access

The key to getting maximum performance from `lazy_beve` is accessing keys in document order. The parser maintains a position pointer and continues scanning from where it left off.

### How Progressive Scanning Works

    std::map<std::string, int> data{{"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", 5}};
    std::vector<std::byte> buffer;
    glz::write_beve(data, buffer);

    auto result = glz::lazy_beve(buffer);
    if (result) {
        auto& doc = *result;

        // FAST: Sequential access - O(n) total
        doc["a"].get<int64_t>();  // Scans from start, finds "a"
        doc["b"].get<int64_t>();  // Continues from after "a", finds "b"
        doc["c"].get<int64_t>();  // Continues from after "b", finds "c"
        doc["d"].get<int64_t>();  // Continues from after "c", finds "d"
        doc["e"].get<int64_t>();  // Continues from after "d", finds "e"
        // Total: scanned the object once
    }

### Performance Comparison

Access Pattern | Complexity | Example
--- | --- | ---
Sequential (in document order) | O(n) total | `a`, `b`, `c`, `d`, `e`
Reverse order | O(n) per access | `e`, `d`, `c`, `b`, `a`
Random order | O(n) per access | `c`, `a`, `e`, `b`, `d`

### Wrap-Around Behavior

If you access a key that appears earlier in the document, the parser wraps around:

    doc["c"].get<int64_t>();  // Position now after "c"
    doc["a"].get<int64_t>();  // Wraps: scans from "c" to end, then start to "a"

This still works correctly but is slower than sequential access.

### Reset Parse Position

If you need to re-scan from the beginning:

    doc.reset_parse_pos();  // Next access starts from beginning

## Type Checking

Check the type of a value before extracting:

    auto& doc = *result;
    auto value = doc["field"];

    if (value.is_object()) { /* ... */ }
    if (value.is_array()) { /* ... */ }
    if (value.is_string()) { /* ... */ }
    if (value.is_number()) { /* ... */ }
    if (value.is_boolean()) { /* ... */ }
    if (value.is_null()) { /* ... */ }

    // BEVE-specific: check array type
    if (value.is_typed_array()) { /* homogeneous elements */ }
    if (value.is_generic_array()) { /* heterogeneous elements */ }

    // Explicit bool conversion - true if not null/error
    if (value) {
        // Value exists and is not null
    }

## Supported Types for get<T>()

Type | Description
--- | ---
`bool` | Boolean values
`int32_t`, `int64_t` | Signed integers
`uint32_t`, `uint64_t` | Unsigned integers
`float`, `double` | Floating-point numbers
`std::string` | String value
`std::string_view` | String view (zero-copy)
`std::nullptr_t` | Null values

## Error Handling

All operations return values that can be checked for errors:

    auto result = glz::lazy_beve(buffer);
    if (!result) {
        // Parse error
        auto error = result.error();
        std::cout << "Error code: " << static_cast<int>(error.ec) << "\n";
        return;
    }

    auto& doc = *result;
    auto value = doc["missing_key"];

    if (value.has_error()) {
        // Key not found or type error
        auto ec = value.error();
        // Handle error...
    }

    auto num = doc["field"].get<int64_t>();
    if (!num) {
        // Extraction failed (wrong type, parse error, etc.)
        auto error = num.error();
        // Handle error...
    }

## Container Methods

    auto& doc = *result;
    auto arr = doc["items"];

    // Check if container is empty
    if (arr.empty()) { /* ... */ }

    // Get number of elements (O(1) in BEVE - count is stored)
    size_t count = arr.size();

    // Check if object contains a key
    if (doc.root().contains("name")) { /* ... */ }

## Size Accessor

Get string lengths or element counts without extracting values - useful for validation:

    auto result = glz::lazy_beve(buffer);
    if (result) {
        // Validate string length before extracting
        if (result->size["username"] > 50) {
            // Username too long - reject without parsing
            return error;
        }

        // Check array size
        if (result->size["items"] == 0) {
            // Empty items array
        }

        // Also works with index access
        size_t first_name_len = result->size[0];  // For arrays
    }

The `size()` method returns: - Strings: character length - Arrays: element count - Objects: number of keys - Primitives: 0

You can also call `size()` directly on any view:

    auto username = (*result)["username"];
    if (username.size() > 50) {
        // Too long
    }

## Deserializing into Structs

Use `glz::read_beve()` to deserialize a lazy view directly into a typed struct:

    struct User {
       std::string name;
       int age;
       bool active;
    };

    struct Container {
       User user;
       int version;
    };

    Container original{{"Alice", 30, true}, 1};
    std::vector<std::byte> buffer;
    glz::write_beve(original, buffer);

    auto result = glz::lazy_beve(buffer);
    if (result) {
        // Navigate lazily to "user", then deserialize into struct
        User user{};
        auto ec = glz::read_beve(user, (*result)["user"]);

        // user.name == "Alice", user.age == 30, user.active == true
    }

This works because Glaze provides a `read_beve` overload that accepts `lazy_beve_view` directly. The lazy navigation skips other fields entirely, and deserialization is single-pass.

### Why Use This Pattern?

This hybrid approach gives you the best of both worlds:

1. Lazy navigation: Skip large sections of BEVE you don't need
2. Fast deserialization: Use Glaze's optimized struct parsing for the parts you do need
3. Type safety: Get compile-time checked structs instead of runtime field access

### Deserializing Array Elements

Combine with indexed views for efficient random access deserialization:

    struct Person {
       std::string name;
       Address address;
    };

    struct Container {
       std::vector<Person> people;
    };

    Container original{{{"Alice", {...}}, {"Bob", {...}}, ...}};
    std::vector<std::byte> buffer;
    glz::write_beve(original, buffer);

    auto result = glz::lazy_beve(buffer);
    if (result) {
        // Build index for O(1) random access
        auto people = (*result)["people"].index();

        // Deserialize only the 500th person
        Person person{};
        glz::read_beve(person, people[500]);
    }

### Alternative: `read_into()` Member Function

If you prefer member function syntax, use `read_into()`:

    User user{};
    (*result)["user"].read_into(user);  // Equivalent to glz::read_beve(user, view)

### The `raw_beve()` Method

Returns a `std::string_view` of the raw BEVE bytes for any lazy view. Use this when you need the binary data itself (for forwarding, storage, or separate parsing):

    auto result = glz::lazy_beve(buffer);

    // Get raw BEVE for different values
    auto root_bytes = result->root().raw_beve();
    auto user_bytes = (*result)["user"].raw_beve();

> Note: For deserialization, use `glz::read_beve(value, view)` instead of `glz::read_beve(value, view.raw_beve())` for better performance.

## Best Practices

1. Access keys in document order: This is the most important optimization. Sequential access gives O(n) total complexity.

2. Store the document reference: To benefit from progressive scanning, use the same document object:

       auto& doc = *result;  // Store reference
       doc["a"];  // Position tracked in doc
       doc["b"];  // Continues from where "a" left off

3. Use iterators when order is unknown: If you don't know the key order or need all keys:

       for (auto& item : doc.root()) {
           // Always efficient - iterates in document order
       }

4. Use `.index()` for multiple random accesses: If you need to access many elements by index or iterate multiple times:

       auto items = doc["items"].index();  // Build index once
       auto first = items[0];              // O(1) access
       auto last = items[items.size()-1];  // O(1) access

5. Keep BEVE buffer alive: The lazy parser stores pointers into the original buffer - it must remain valid for the lifetime of the document.

6. Use `std::string_view` for strings: When you don't need a copy, `get<std::string_view>()` provides zero-copy access.

7. Access few fields for best speedup: Lazy BEVE shines when you access 1-5 fields from a large document. For full deserialization, use `glz::read_beve`.

8. Use `glz::read_beve(value, view)` for struct deserialization: Glaze provides an overload of `read_beve` that accepts `lazy_beve_view` directly.

## Comparison with All Approaches

Feature | `glz::read_beve` | `glz::lazy_beve` | `lazy_beve` + `.index()`
--- | --- | --- | ---
Parse time | O(n) | O(1) | O(1) + O(n) on index
Field access | O(1) | O(k)* | O(1) after index
Random array access | O(1) | O(k)* | O(1) after index
Memory usage | Struct size | ~48 bytes | ~48 + 8n bytes
Type safety | Compile-time | Runtime | Runtime
Best for | Full deser. | Few accesses | Many accesses

*k = bytes to skip to reach field

## See Also

- BEVE (Binary) - BEVE format documentation
- Lazy JSON - Lazy parsing for JSON format
- Reading - Standard reading with `glz::read`