DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

Codegen and readOnly/writeOnly: Why Create Models Include Server Fields

Quick answer: The readOnly and writeOnly keywords exist precisely so a generator can split one schema into the shape a client receives and the shape it sends. If your generated create models still include server-owned fields like id and createdAt, the generator is not honoring those flags, and your team will either send fields the server ignores or fight the type system every time. Treat them as a hard boundary, not a documentation hint.

The issue surfaced in a generated TypeScript client. The User model listed id, createdAt, and updatedAt as required, and the create endpoint referenced that same model. Engineers constructing a new user had to invent an id and timestamps to satisfy the compiler, even though the server always discarded them. Some teams cast around the type; others sent made-up values. The spec was actually correct, but the generated model had collapsed the request and response views into one.

What the two keywords mean

The semantics are complementary and easy to remember once framed around the direction of data:

  • readOnly: true means the property is included in responses but never in a request. Server-owned identifiers and timestamps are the classic examples.
  • writeOnly: true means the property may be sent in a request but never returned in a response. A password is the canonical case; it is accepted on write but should not appear when reading the resource.

Here is the spec fragment that should drive two different generated views.

A generator that respects the flags produces a response type containing id and createdAt but not password, and a request type containing password but not the server-owned fields.

Why collapsing the views causes real problems

The failure is not just cosmetic. A single flattened model pushes the wrong behavior into both directions. The table below captures what breaks when the boundary is ignored.

Field class Single flattened model Correct split models
readOnly id/timestamps Required on create; clients fake values Absent from request types entirely
writeOnly password Leaks into response types Only present on the write model
Type safety Cast or as any to compile Create payload matches what the server accepts
Runtime behavior Extra fields sent and silently ignored No invalid fields can be constructed

How a good generator models the split

Some generators emit separate types automatically; others use a base schema with request and response variants. The generated TypeScript should read roughly like this, with the create type built from the writable properties and made partial where optional.

If your current generator cannot produce separate views, you have two honest options: switch to a generator that honors the keywords, or define explicit request and response schemas in the spec instead of relying on one model. The latter is verbose but unambiguous and works with any tool.

Mark the flags at the source, not in generated code

Resist the temptation to hand-edit generated files to remove the fields. The next generation run overwrites the change, and the committed output drifts from the spec. Put readOnly and writeOnly on the source properties and regenerate. If a field is genuinely shared and writable in both directions, leave it unflagged; the default is to include a property in both views, which is usually the right behavior for ordinary data.

Validate the generated boundary with a test

Add a compile-time check that a create payload built without any server-owned field is accepted, and one that would fail if a readOnly property appeared in the request type. For writeOnly secrets, assert the response type does not expose the field. These checks fail the moment a generator version or a spec change reintroduces the flattened view, which is far cheaper than discovering fake IDs in production requests.

How required interacts with the split

One subtlety trips up teams even after the views are split: a property listed in the schema's required array is only required within the view where it appears. A readOnly id can be required on the response model while being absent from the create model, and a writeOnly password can be required on the request without breaking the response type. Keep the required list focused on fields that genuinely must be present; do not remove a server-owned field from required just to make the create payload compile. When the generator honors the direction flags, the two requirements coexist without conflict.

FAQ

Can I just omit readOnly fields when I build the request?

You can, but a flattened model often marks them required, so the type system forces you to include them anyway. The durable fix is to let the generator split the views so invalid requests cannot be constructed.

Why does a password disappear from the response type?

Because writeOnly properties are accepted on write but excluded from read responses. That is intentional and prevents a secret from being exposed through a generated client or a logged response object.

What if my generator ignores these keywords?

Either use a generator that honors them or define explicit request and response schemas in the spec. Do not hand-edit generated output, since regeneration will silently revert it.

Should ordinary editable fields be flagged?

No. Leave normal data that appears in both requests and responses unflagged. Use readOnly for server-owned output and writeOnly for input-only secrets, and the default handles the rest.

This is part of the Code Generation Guide — start there for the broader set of decisions that keep generated clients aligned with the contract.

Top comments (0)