-   [](https://platform.exaloop.io/)
-   Data APIs
-   Generated Message API

# Generated Message API

Generated Protobuf classes expose typed fields and collections alongside parsing, serialization, copying, and presence operations. This reference describes their behavior, including how child objects remain attached to a parent during mutation. Start with the [Protobuf guide](https://platform.exaloop.io/data/protobuf/) to generate your first module.

The collection example uses downloadable [fixture.proto](https://platform.exaloop.io/assets/files/fixture-61e39663138c3544cf9ff971b623f133.proto/) and [common.proto](https://platform.exaloop.io/assets/files/common-97c1fc821d1d6dad7fdecea79f2cda08.proto/) schemas. This is the generated class API, rather than the [runtime reflection API](https://platform.exaloop.io/data/protobuf-dynamic/).

## Message Operations

A new message starts with its schema's defaults. You can populate its fields before calling `to_bytes()`, or use `from_bytes()` to construct it from wire data. For an existing object, `parse()` replaces its contents only if parsing succeeds.

| Operation | Contract |
| --- | --- |
| `MessageType()` | Empty/default-initialized message; required fields may still be missing |
| `MessageType.from_bytes(data: bytes)` | Parse and validate a new message |
| `message.parse(data: bytes)` | Replace only on success; failure preserves values and live attachments |
| `message.to_bytes() -> bytes` | Validate required fields/depth and encode deterministically |
| `message.has(field: str or int) -> bool` | Presence by original schema name or field number |
| `message.clear(field: str or int)` | Reset field/default and detach old live values |
| `message.clone()` | Detached deep copy |
| `message.CopyFrom(other: MessageType)` | Checked deep replacement; message types must match |
| `message.SetInParent()` | Mark this child present without assigning a field |
| `message.close()` | Clear/invalidate a root or detached message; rejects attached children |
| `with message as entered:` | Check on entry and close on exit |

Messages are owned by Codon's garbage collector, so `close()` is not required to free a foreign message handle. It is an explicit way to clear and invalidate an object. Closing a parent detaches any children or collection views you retained; those objects remain usable on their own.

Operations on a closed message raise `ValueError`, as do invalid wire input, missing required fields, unsupported presence checks, out-of-range values, and invalid helper indices. Collection indexing and deletion use the usual `IndexError` and `KeyError`. Shared messages need external synchronization during mutation.

## Wire Compatibility

Generated messages support scalar types, packed and unpacked repeated fields, maps, proto2 defaults and required fields, proto3 optional fields, and oneofs. Strings must be valid UTF-8; bytes fields accept arbitrary data.

Repeated occurrences of a singular scalar take the last value. Nested message occurrences merge, with required fields checked after parsing the complete message. Unknown fields survive parsing and serialization.

Open enums preserve unknown numeric values. Closed-enum setters reject them, while parsing retains unknown wire values as unknown fields, including Protobuf's duplicate-map-value rules. Serialization orders known fields by number and maps by key. This is deterministic, not a canonical encoding shared by all runtimes.

| Feature | Support |
| --- | --- |
| Maximum encoded buffer | 2 GiB minus one byte |
| Maximum nesting depth | 100 |
| Editions, extensions, groups | Not supported |
| RPC/gRPC, JSON and text APIs | Not generated |
| Keyword-field constructors and keyword arguments to `add()` | Not supported |
| Well-known types | Ordinary generated messages, without specialized helpers |

## Fields and Numeric Types

Read and assign a singular field with `message.field` and `message.field = value`. The generated type reflects its Protobuf width: an `int32` field returns `i32`, for example. Setters also accept selected wider types and check their range before conversion.

| Schema type | Native storage/getter | Setter input |
| --- | --- | --- |
| `int32`, `sint32`, `sfixed32`, enum | `i32` | `i32` or range-checked `int` |
| `uint32`, `fixed32` | `u32` | `u32` or range-checked `u64` |
| `int64`, `sint64`, `sfixed64` | `int` | `int` |
| `uint64`, `fixed64` | `u64` | `u64` |
| `float` | `float32` | `float32` or `float` |
| `double` | `float` | `float` |
| `string`, `bytes`, `bool` | `str`, `bytes`, `bool` | Same native type |
| Message | Typed live child | Same message type, copied on assignment |

The method forms, `get_field()` and `set_field(value)`, are equivalent to attribute access for singular fields. Repeated fields additionally offer `add_field(value)` and `set_field(index, value)`, and maps offer `put_field(key, value)`. These helpers perform the same range and copying checks; their repeated-field indices must be nonnegative. Enum declarations provide `i32` constants.

When a field name conflicts with a keyword, builtin, dunder name, or generated member, its property gains a trailing underscore. A schema field named `class` is therefore accessed as `class_`; a field named `clear` becomes `clear_`. Name-based operations still use the schema's original name, as in `message.has("class")`, and the accessor is `get_class()`. The generated module shows the final spelling if additional collisions require more underscores.

## Repeated Views

Repeated fields behave like typed, mutable sequences. Both the property and `get_field()` return the same live view, so appending through either updates the message. Indexing supports negative values. A slice creates a new list, but any message elements in that list are still the original live children.

| Member | Behavior |
| --- | --- |
| `len(view)`, `bool(view)`, iteration, `view[index]` | Inspect live elements |
| `view[start:stop:step]` | Native list of selected elements |
| `append(value)`, `extend(values)`, `insert(index, value)` | Validate inputs; message values are copied |
| `add()` | Create/return attached message element; unavailable for scalars |
| `view[index] = value`, `view[slice] = values` | Scalar-only assignment with conversion/range checks |
| `del view[index]`, `del view[slice]` | Remove and detach removed children |
| `pop(index=-1)` | Remove/return value; message becomes detached |
| `remove(value)`, `count(value)` | Native value comparison |
| `index(value, start=0, stop=9223372036854775807)` | Find matching value or raise `ValueError` |
| `clear()`, `reverse()`, `sort(key=None, reverse=False)` | Mutate collection; custom keys are useful for messages |
| `copy()` | Native list, deeply copying message elements |
| Equality / representation | Native container semantics with compatible element types |

To replace the contents of an existing repeated child, call `view[index].CopyFrom(other)`. Direct assignment to a message element or message slice is not supported. For operations such as sorting, provide a key based on the fields you want to compare; generated messages do not implement Python's complete message-comparison API.

## Map Views

Maps expose typed key/value collections. One behavior differs from an ordinary dictionary: looking up a missing key with `view[key]` inserts its default value. Use `get()` or `key in view` to inspect a map without changing it.

| Member | Behavior |
| --- | --- |
| `len(view)`, `bool(view)`, iteration, `key in view` | Inspect keys; membership does not insert |
| `view[key]`, `get_or_create(key)` | Insert default scalar/empty child when missing |
| `get(key, default=None)` | Optional value without insertion |
| `view[key] = value` | Scalar-valued maps only; validates key and value |
| `del view[key]`, `pop(key)`, `pop(key, default)`, `popitem()` | Remove/detach and return values as applicable |
| `update(mapping)`, `setdefault(key, default)` | Scalar-valued maps only |
| `clear()` | Empty map and detach children |
| `keys()`, `values()`, `items()` | Native dictionary views |
| `copy()` | Native dict, deeply copying message values |
| Equality / representation | Compatible native dictionary semantics |

For message-valued maps, mutate the child returned by `view[key]`, or copy another message into it with `CopyFrom()`. The generated `put_field(key, value)` helper also supports copying a message into a map. Unlike indexing, a missing `get()` result does not create an entry or mark the field present.

## Whole-Collection Assignment

You can replace an entire repeated field or map with a native list or dictionary. Here the fields use Protobuf's 32-bit integers, so the collection literals use `i32` elements:

```python title="Source"
from fixture_pb import Fixture

message = Fixture()
message.signed32s = [i32(-1), i32(2)]
message.counts = {"visits": i32(3)}
message.signed32s.append(4)
assert message.counts["visits"] == i32(3)
```

Save both schemas in the current directory, then generate their modules:

```sh title="Terminal"
exaloop protoc -I. --codon_out=. common.proto fixture.proto
exaloop --with data run -release program.codon
```

The Codon example above belongs in `program.codon`. Assignment validates and copies all elements before replacing the field, so an invalid value leaves the old collection unchanged. Assigning `[]` or `{}` clears the field. To assign another message's collection, use `other_view.copy()` to obtain a native container.

### Retained Children

Clearing or replacing a field detaches its old children and views. A retained reference remains a valid object, but later changes to it no longer affect the parent. Selecting another oneof member and successfully parsing replacement data have the same effect. An unset child that has only been read remains eligible to become present on its first mutation.

All of these rules are maintained by the public attributes and methods. Private storage such as `_f<number>`, ownership state, and codec hints is not part of the message API.

Source: https://platform.exaloop.io/data/protobuf-api/
