Answer the Exact Question First
When you change a field from optional to required in a tRPC procedure, the server will immediately reject any request that omits that field with a ZodValidationError. tRPC has no built‑in rollback or grace‑period mechanism, so the only ways to keep clients working are to either keep the field optional, supply a default, or add explicit version handling so the server can accept the old shape for a while.
Current Behavior
- tRPC validates every incoming request against the Zod schema before the procedure runs.
- If a required key is missing, Zod throws
ZodValidationError and the procedure is aborted.
- There is no automatic version negotiation; the server always enforces the current schema.
Confirmed Facts
- Changing
z.string().optional() to z.string() makes the field mandatory.
- The error message will reference the missing key exactly as defined in the schema.
- Updating the client code to match the new schema and recompiling the server resolves the error.
Safe Roll‑Back Strategies
1. Keep the Field Optional During Transition
Mark the field as optional (z.string().optional()) until all clients are updated. Once you’re confident every client sends it, switch to required.
2. Provide a Default Value
Use z.string().default('') or a more meaningful default so the server can proceed even if the client omits the field.
3. Add a Version Header or Query Parameter
Introduce a lightweight header (e.g., X-Client-Version) or query param that indicates the client’s schema version. In a middleware you can branch validation:
app.use((req, res, next) => {
const clientVer = req.headers['x-client-version'] || '1.0';
req.clientVer = clientVer;
next();
});
Then, in the router, conditionally apply the stricter schema if the client version is newer.
4. Grace‑Period Middleware
Wrap the router with a middleware that detects the version header and, for older versions, temporarily uses the legacy schema. Example:
import { z } from 'zod';
const legacySchema = z.object({ fieldName: z.string().optional() });
const newSchema = z.object({ fieldName: z.string() });
app.use((req, res, next) => {
const schema = req.clientVer === '2.0' ? newSchema : legacySchema;
try {
schema.parse(req.body);
next();
} catch (e) {
next(e);
}
});
This keeps the old shape accepted until you decide to drop the legacy path.
5. Client‑Side Version Negotiation
If you can’t change the server, the safest approach is to let the client detect the error and retry with the new payload. A simple retry loop that adds the missing field after receiving a ZodValidationError can keep the UI responsive.
When to Ask for More Diagnostics
If you don’t already send a version identifier, the recommendation above will lean toward adding one. Knowing whether your clients already expose a version header or can be updated to do so changes the exact middleware strategy you should adopt.
Question for You
Do your current requests include any version‑indicating header or query parameter that you could leverage for a graceful transition?
Practical Verification Steps
- Inspect the tRPC router and confirm the field’s required status.
- Log
req.body in the middleware before validation to see the actual payload.
- Send a test request (via Postman or curl) that includes the field; the procedure should succeed.
- If you add a version header, test both legacy and new versions to ensure the correct schema is applied.
Security Note
Always catch ZodValidationError in a global error handler and return a sanitized message to the client. Exposing stack traces can leak implementation details.