Skip to content

Accept init shapes in Standard Schema validation - #169

Merged
timostamm merged 6 commits into
bufbuild:mainfrom
Adrastopoulos:standard-schema-init-shapes
Sep 4, 2026
Merged

Accept init shapes in Standard Schema validation#169
timostamm merged 6 commits into
bufbuild:mainfrom
Adrastopoulos:standard-schema-init-shapes

Conversation

@Adrastopoulos

@Adrastopoulos Adrastopoulos commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #95.

createStandardSchema() only accepts message instances. A plain object fails before any rule is evaluated, even when the data is valid:

const schema = createStandardSchema(UserSchema);
await schema["~standard"].validate({ email: "test@example.com" });
// { issues: [{ message: "Cannot validate message undefined with schema example.User" }] }

Standard Schema consumers usually hold plain values, not message instances: form state, or deserialized RPC payloads (e.g. TanStack Start's inputValidator).

Changes:

  • validate() now converts plain objects to messages with create() before validating. Message instances are validated directly, as before. Errors thrown by create() become issues, since Standard Schema validators must not throw.
  • createStandardSchema() keeps its declared types. The Standard Schema input parameter is covariant, so widening it to MessageInitShape would break existing consumers, e.g. TanStack Form with defaultValues: create(Schema).
  • New createStandardSchemaInit() shares the runtime but declares MessageInitShape as its input, for consumers where the schema's input type dictates what callers pass. MessageInitShape is not assignable to MessageShape, so one input type cannot serve both.

@CLAassistant

CLAassistant commented Jul 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@timostamm

Copy link
Copy Markdown
Member

Thanks for the PR!

I need some help understanding the changes.

The Standard Schema input parameter is covariant, so widening it to MessageInitShape would break existing consumers, e.g. TanStack Form with defaultValues: create(Schema).

MessageInitShape is either the fully materialized message, or a partial message where all fields are optional, see https://github.com/bufbuild/protobuf-es/blob/v2.14.1/packages/protobuf/src/types.ts#L195.

I think that means it should be possible to simplify the changes?

validate() now converts plain objects to messages with create() before validating. Message instances are validated directly, as before. Errors thrown by create() become issues, since Standard Schema validators must not throw.

Makes sense. When create throws, there is nothing to validate further. So I believe that the new createStandardSchemaInit could simply wrap the existing function, no? That would probably make the code paths easier to follow, and be optimal for bundle sizes.

The Standard Schema validator previously required a message instance
created with create(). Plain objects failed with "Cannot validate
message undefined" before any rule was evaluated, even for valid data.

Standard Schema consumers usually hold plain values, not message
instances: form libraries validate form state, and RPC frameworks
validate deserialized JSON payloads. Requiring create() at every
callsite defeats the interop the interface exists for.

createStandardSchema now converts a plain object to a message with
create() before validating; message instances are still validated
directly. Its declared types are unchanged, so existing consumers,
such as TanStack Form with defaultValues: create(Schema), see no
type-level change.

The new createStandardSchemaInit shares the same runtime but declares
MessageInitShape as its input type, for consumers whose values are
plain objects: form state typed from plain default values, or RPC
input validation where the schema's input type dictates what callers
may pass.

Errors thrown by create() for malformed init values are reported as
issues instead of escaping the validator.

Fixes bufbuild#95.
@Adrastopoulos
Adrastopoulos force-pushed the standard-schema-init-shapes branch from a7fc1f7 to ac1b169 Compare September 3, 2026 22:00
@Adrastopoulos

Copy link
Copy Markdown
Contributor Author

Yes, you're right on both counts. We can just return MessageShape since it's assignable to MessageInitShape. Indeed, createStandardSchemaInit can just be a thin wrapper.

@Adrastopoulos

Adrastopoulos commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

I also realized that create() doesn't throw. It already checks if the input is a Message instance, hence our try/catch duplicated that check. Now it's even simpler.

@timostamm
timostamm merged commit d7b07cd into bufbuild:main Sep 4, 2026
14 checks passed
@Adrastopoulos
Adrastopoulos deleted the standard-schema-init-shapes branch September 4, 2026 10:45
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.

[Feature Request] Better integration with forms

3 participants