Skip to content

fix: accept an empty proto3 string when decoding a record - #312

Closed
moetemp wants to merge 9 commits into
mainfrom
moe/up/proto3-empty-string
Closed

moetemp wants to merge 9 commits into
mainfrom
moe/up/proto3-empty-string

Conversation

@moetemp

@moetemp moetemp commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

What changed?

This PR makes the generated Python and TypeScript decoders accept an empty proto3 string or
bytes field that has no presence.

The first commit adds tests to the stream-service sample, Brook's Stream service, that decode a
CreateStreamRequest with an empty stream_id. They expect today's error. The second commit is
the fix, so its diff shows only what it changes.

  • Python drops the if not value.<field> check.
  • TypeScript reads proto.<field> ?? "" (or ?? new Uint8Array() for bytes), where it wrapped
    the field in requiredField.
  • .NET already reads such a field as is, so its output doesn't change.

Message fields, optional fields and oneof members keep their presence check.

Why?

A plain proto3 scalar has no presence, so "" is the same as unset. But a WIT string bound to
such a field decoded "" as a missing required field in Python and TypeScript.

Brook's Stream service hits it. Its create-stream-request has stream-id: string, bound to a
plain string stream_id in temporalio/api#895. On native, an empty id asks the server for one.
So a Stream handler couldn't decode such a create.

TypeScript also reads a field that isn't there as empty, since proto3 JSON leaves out a field
that has its default value.

How did you test it?

New Python and TypeScript tests decode a CreateStreamRequest with an empty stream_id, as a
Stream handler does. The TypeScript test also decodes one without a streamId key, and the Python
test encodes the request back to the same wire message.
It sits on #296. The stack is #308, #310, #311, #297, #296, then this PR.
cargo validate passes.

Brook's writer uses records of the stream service across packages. The match takes the package
and the interface, since several samples share `temporal:nexus`.
@moetemp
moetemp force-pushed the moe/up/proto3-empty-string branch from 07890cb to e87464a Compare October 27, 2026 18:48
The samples now generate what Brook generates, byte for byte from its WIT. No SDK has the Stream
protos yet, so the sample projects ship their bindings. Today only part of the output builds, and
each fix turns its part on.
…today's output.

Today's Python and TypeScript generation panics on it, so their outputs keep their previous
content. .NET types each case as the raw proto message, not as its record.
Variant planning kept such a case as the raw proto message. Python and TypeScript then
panicked on a message conversion that wasn't model-shaped, and .NET typed the case as the
proto class. The case now plans as the record, as a record field already does. Brook's stream
service now generates for TypeScript, and its .NET read item holds the records.
A proto in temporal/... always became a temporalio module, so a proto that the SDK doesn't have
yet couldn't be imported. --python-proto-module, and python_proto_modules in examples.toml, map a
file path prefix to the module that replaces it. Brook's stream service now generates for
Python, with the Stream protos from the sample's own package.
A proto `uint64` field was planned as a signed `s64`, so .NET typed it as `long` against the
`ulong` of its proto class and the model didn't compile. The `writer-id` of Brook's writer sample
shows the fix, and the writer now builds for .NET.
A proto-backed request holds a 64-bit integer as a `Long`, but a resource method takes a number.
The method body converts its 64-bit params and fields, and lists of them. A `u64` becomes an
unsigned `Long`. Brook's stream service now type-checks.
…ay's error.

On native, Brook's empty `stream-id` asks the server for an id, and `CreateStreamRequest.stream_id`
is a plain proto3 `string`. Python and TypeScript reject an empty id today, so the tests expect
that error.
Python and TypeScript rejected an empty `string` or `bytes` field without presence as a missing
required field. Such a field has no presence, so empty is the same as unset. The decoders now
accept it, and TypeScript reads an unset field as empty. A native create with Brook's empty
`stream-id`, which asks the server for an id, now decodes.
@moetemp
moetemp force-pushed the moe/up/proto3-empty-string branch from e87464a to 94dd887 Compare October 28, 2026 00:21
@moetemp

moetemp commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Closing this. It drops nexgen's documented rule that a required string is non-empty, and it hides a missing proto2 string. Brook doesn't need it, since its stream-id stays required and non-empty.

@moetemp moetemp closed this Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant