Using both models

One schema, both decoders - in one translation unit, even mid-decode. Back to the README.

Each model has its own root namespace: arena at rp::arena::<pkg>::Msg, streaming at rp::stream::<pkg>::Msg, with the schema’s enums as a single shared type at rp::common::<pkg>, aliased into both. Nothing the generator invents lands inside your package, so a schema coexists unless it declares a type where a root namespace goes (below). Generate both (--arena --stream, or GENERATOR both in CMake) and use each where it fits:

#include "person.rp.hpp"         // arena:     rp::arena::example::Person
#include "person.rp.stream.hpp"  // streaming: rp::stream::example::Person  (both pull in the shared enums)

namespace ex  = rp::arena::example;   // alias each model once
namespace ex_s = rp::stream::example;

const ex::Person* tree = ex::Person::decode(bytes, arena);   // materialize when you need an object
const auto st = ex_s::Person{bytes}.decode( /* … */ );        // or stream when you don't
if (!st.ok()) { /* malformed input */ }                       // decode() is [[nodiscard]]
// ex::Status and ex_s::Status are the same enum type.

The models also combine mid-decode: stream a large outer message and materialize just the sub-messages you keep. A streaming sub-decoder’s rp_bytes() is exactly the sub-message’s field bytes, which the arena decode() accepts directly. The materialized tree borrows those bytes (a slice of the input wire), so wire must outlive every tree you keep:

rapidproto::Arena arena;
ex_s::Person{wire}.decode(
    [&](ex_s::Person::address, ex_s::Address a) {
        const ex::Address* tree = ex::Address::decode(a.rp_bytes(), arena);
        // keep `tree` - valid while both `wire` and `arena` live
    });

Every tree materialized this way accumulates in the arena until you reset() it. If a tree is a per-element temporary (use, then discard), reset between uses - even from inside a callback, mid-walk: the streaming side borrows the input buffer, never the arena, so rewinding it there is safe and keeps a long stream’s memory flat.

A runnable end-to-end example (one schema, both models in one TU, including the mid-decode hybrid) is in examples/consumer.

Coexisting with protoc

Nothing to do: generated code lives under rp::, protoc’s under your package, so person.pb.h and person.rp.hpp coexist in one TU - including for schemas importing a well-known type, where protoc also defines google::protobuf::Timestamp.

One rule carves out an exception: a type may not sit where a root namespace goes. There are two ways to hit it: your own code declares a type at rp::arena (a plain namespace rp is fine - namespaces merge), or the schema declares one there - package rp; with a top-level message/enum named arena, stream or common, or a package-less schema with one named rp - and the collision surfaces in the generated header:

c.rp.common.hpp: error: 'namespace rp::common { }' redeclared as different kind of entity

Both are fixed by renaming the root with --namespace-prefix. Nested types and sub-packages are unaffected - protoc mangles a nested type to Outer_Inner, and namespaces merge.