ZodValidationError: fieldName is required after making field required in tRPC procedure
0 reputation · 02 Jun 2024, 07:11 UTC
0 reputation · 02 Jun 2024, 07:11 UTC
The goal is to determine whether tRPC can safely roll back or tolerate a breaking schema change without causing client‑side validation failures. Currently, when a procedure’s Zod schema is altered—for example, by changing an optional field to required—the server validates incoming requests against the new schema and throws a ZodValidationError if the client still sends the old payload. There is no built‑in version‑negotiation or grace period that would allow the server to accept the deprecated shape for a limited time.
Constraints include the lack of automatic rollback mechanisms, the risk of UI degradation when clients are not updated simultaneously, and the reliance on client‑side error handling to detect mismatches. An open design question is whether tRPC should introduce optional schema versioning or a configurable tolerance window that permits deprecated fields before enforcing the new schema strictly.
What mechanisms could tRPC adopt to provide a safe rollback after a schema change? Should tRPC support a version header or similar signal to allow temporary backward compatibility? Is a grace‑period approach feasible without complicating the middleware pipeline?
30275 reputation · 02 Jun 2024, 08:51 UTC
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.
ZodValidationError and the procedure is aborted.z.string().optional() to z.string() makes the field mandatory.Mark the field as optional (z.string().optional()) until all clients are updated. Once you’re confident every client sends it, switch to required.
Use z.string().default('') or a more meaningful default so the server can proceed even if the client omits the field.
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.
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.
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.
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.
Do your current requests include any version‑indicating header or query parameter that you could leverage for a graceful transition?
req.body in the middleware before validation to see the actual payload.Always catch ZodValidationError in a global error handler and return a sanitized message to the client. Exposing stack traces can leak implementation details.
Use comments to ask for clarification. Post a solution as an answer.
30,275 reputation · 02 Jun 2024, 09:51 UTC
While version headers are effective for long-term API lifecycles, a more immediate way to handle the transition from optional to required fields is using z.union or z.or. Instead of a hard switch, you can temporarily define the input schema as a union of the legacy shape and the new required shape:
const inputSchema = z.union([
z.object({ fieldName: z.string() }), // New required shape
z.object({ fieldName: z.string().optional() }), // Legacy shape
]);
This allows the server to accept both payloads during the deployment window. To verify this is working, you can check the ZodValidationError.issues array; if the union is too permissive, Zod may return multiple errors. A strict union ensures that as long as the payload matches either the old or new contract, the procedure executes. Once client telemetry confirms the legacy shape is no longer in use, the union can be collapsed into the single required schema.