Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guidance on creating slight schema variants for varied contexts #85

Closed
chrbala opened this issue Jul 13, 2020 · 5 comments
Closed

Guidance on creating slight schema variants for varied contexts #85

chrbala opened this issue Jul 13, 2020 · 5 comments
Labels
documentation Improvements or additions to documentation

Comments

@chrbala
Copy link
Contributor

chrbala commented Jul 13, 2020

I am interested in using zod for frontend, API, and backend validation.

For example, a browser might validate user inputs live as they are changed on the page. The user hits "submit" which sends the data to the API. The API runs its own validations on this data that may be somewhat different. The API changes the data slightly to conform with the backend needs. The backend has a similar schema, but there are some small differences.

Differences might be things like:

  • Adding a field deep in the schema
  • Swapping a foreign key ID for an object pulled from some other location
  • API allows partial inputs for updates, but the backend sometimes requires complete inputs. For example, take a blog post draft that can be missing a title field, but the backend requires a title to be persisted before publishing.

I'm not necessarily looking for changes to the library, but rather recommended patterns through documentation, blog posts, etc. I see that zod supports merging, masking, and extending, but am looking for higher level guidance on patterns for this type of use case. I can see schema sharing as something that could potentially make a codebase much more complex without due care. The guidance here might be to create separate schemas because the costs of coupling schemas across contexts is too high!

There may also be features that could be added to zod that make this use case easier.

@colinhacks
Copy link
Owner

I think I've held off on documenting higher-order patterns because I have plans for implemented separate libraries on top of Zod for RPC, REST, GraphQL, and forms. Of course if someone beats me to it I won't be mad ;)

Currently I'm already working on the ZodRPC package. This describes the full workflow for using Zod to implement an end-to-end typesafe API. It also includes out-of-the-box client side SDK generation which is pretty rad.

I know you had a broader point but I'm going to go through your bullet points anyway. Maybe it'll shed light on how I think about things.

  • Adding a deep field is something I'm considering. I think I'll add a .patch method to object schemas that lets you certain field schemas (shallowly or deeply)
  • I'd recommend doing this in userland. I think of Zod as a "point source" for type safety in an application. You can confidently parse an object containing a foreign key, fetch&parse the associated foreign record, and swap the object in for the key with full confidence in the inferred type of the composite object. (That said, I hope to implement some chainable data transformation methods here soon - I think I have a good approach in mind. But this particular case is still too niche to merit its own method imo.)
  • Sounds like this could be achieved with the ability to set default values that are set before parsing. This will be possible in the upcoming transformation/codec functionality that should drop in zod@1.10 or 11. I have a simple version working locally but I'm not 100% confident of my approach yet.

@colinhacks colinhacks added the documentation Improvements or additions to documentation label Jul 17, 2020
@chrbala
Copy link
Contributor Author

chrbala commented Jul 18, 2020

Thinking about this one a bit more. I'm wondering if it might make sense to generally solve this type of thing on the application side like this:

// commonValidations.ts
import * as z from 'zod';

export const id = z.string().refine(val => val.length == 8, {
  message: "Foreign keys should be 8 characters",
});
// clientValidations.ts
import * as z from 'zod';
import * as commonValidations from './commonValidations';

/*
  Client does not have any extra validations for ID, so it uses common directly
 */
export const user = z.object({
  id: commonValidations.id,
});
// serverValidations.ts
import * as z from 'zod';

// model only exists on the server, not implemented here
import { isValidId } from './applicationModel';
import * as commonValidations from './commonValidations';

export const id = commonValidations.id.refine(isValidId, {
  message: 'ID is invalid',
});

export const user = z.object({
  id,
});

What I like about this design is that everything is very explicit, clear, and avoids tight coupling between client and server. What I don't like is that it could get pretty verbose with lots of redundancy across the client and server as the schema gets more complex. That is, if user had a schema refinement, it would need to be shared across the client and server as well. Any kind of schema patching would be more concise in exchange for tighter coupling between client and server in this type of situation.

@colinhacks
Copy link
Owner

I think your example is a good reference for best-practices code sharing between client and schema.

Closing for now - I'm not sure how to act on this. There isn't much guidance I could give that would be generally applicable besides maybe "don't repeat yourself". Future readers: if there's a particular feature that would help you avoid redundancy then by all means create a new issue for it.

@chrbala
Copy link
Contributor Author

chrbala commented Aug 3, 2020

Yeah, agreed that at this point there isn't really an obvious action to take here. From investigating this, I've found that it's mostly easiest to make shared types from the leaf nodes up as high as what is fully shared, then do a hard fork where the types diverge. It can get pretty redundant with deeply nested types that diverge near the leaves, but I've found that the redundancy is the clearest way to represent this, rather than abstractions or core feature additions.

@joacoespinosa
Copy link

I appreciate that this issue is closed. Having said that, is there any guidance either of you can share with us, given the current features supported by the library? Thank you in advance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants