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

Data representation of schema? #507

Closed
jasonkuhrt opened this issue Jun 30, 2021 · 9 comments
Closed

Data representation of schema? #507

jasonkuhrt opened this issue Jun 30, 2021 · 9 comments

Comments

@jasonkuhrt
Copy link

Hey,

Given a Zod schema e.g.:

const a = z.object({
  foo: z.string(),
})

Is there a way to get a plain data representation of this?

I am trying to map a Zod schema to a GraphQL schema (specifically an input object type).

@scotttrinh
Copy link
Collaborator

@jasonkuhrt

Perhaps you're looking for ZodObject.shape?
TypeScript Playground

import assert from "assert/strict";
import { z } from "zod";

const a = z.object({
  foo: z.string(),
})

assert.ok(a.shape.foo instanceof z.ZodString)

@jasonkuhrt
Copy link
Author

jasonkuhrt commented Jul 2, 2021

@scotttrinh perhaps, I'll check that out thanks!

IIUC .shape should give enough information to map the defined type/rules to another type system (of course within the bounds of what features overlap between Zod and the target system).

@jasonkuhrt
Copy link
Author

@scotttrinh there is a use-case I'm missing.

I need a way to attach metadata to zod schema fields. I would like to attach something like this:

.metadata({
  graphqlMapping: 'Foo'
})

Where Foo is a name of a type in the GraphQL API.

This is because I have some fields that I want to map to/re-use existing GraphQL types in the schema.

@scotttrinh
Copy link
Collaborator

@jasonkuhrt I don't think you're going to find a way to "attach metadata" to a ZodObject since that is something that Zod itself defines and doesn't know anything about carrying around additional user-defined metadata.

You could invert this a bit if you want to avoid duplication, something like this: TypeScript Playground

import assert from "assert/strict";
import { z } from "zod";

const personDef = {
  name: {
    schema: z.string(),
    graphqlMapping: "Name",
  },
};

const person = z.object({
  name: personDef.name.schema,
})

assert.ok(a.shape.name instanceof z.ZodString)

You could probably even write a helper that would define a schema based on one of these "def" objects, but I'll leave that as an exercise for the reader 😉

@jasonkuhrt
Copy link
Author

I think metadata would be nice to have. Might make a new feature request for that.

import assert from "assert/strict";
import { z } from "zod";

const person = z.object({
  name: z.string().metadata({ graphqlMapping: 'Name' }),
})

assert.ok(person.shape.name instanceof z.ZodString)
assert.ok(person.shape.name.metadata.graphqlMapping === 'Name')

@colinhacks
Copy link
Owner

Hey Jason, cool to see you using Zod. I was an early fan/user of Nexus back in early 2019, great stuff.

I'm opposed to the concept of adding metadata to schemas for reasons explained in #76 and #138. I think it's an antipattern since most people (not you though) want to use it for contextual refinements that break encapsulation. Also TypeScript won't be aware of the type signature of the metadata in most scenarios, e.g. if you're looping over an array of ZodObjects to generate a GraphQL schema (which it seems like you're trying to do).

As Scott suggested, I recommend using a higher-order approach, where your Zod schema is an element of a higher-level type. I describe this pattern is more detail here. For use cases like yours (where you are presumably looping over Zod schemas to generate GraphQL schemas) it's a safer approach, since TypeScript can provide proper typing on your metadata properties. Without this, you usually need to cast to any to prevent errors on metadata property access inside loops:

// assuming that I added a writable `metadata` field to the ZodType base class:
const User = z.object({ name: z.string() });
  const Person = z.object({ age: z.number() });
  const models = [User, Person];
  for (const model of models) {
    for (const fieldName of Object.keys(model.shape)) {
      const field = (model.shape as any)[fieldName];
      const name = field?.metadata?.graphqlMapping || fieldName; // TypeError;
      // do stuff;
    }
  }

Compare this to an "inverted" approach closer to what Scott suggested:

type Field = { schema: z.ZodTypeAny; graphqlMapping?:string; }
type Model = {[k:string]: Field};
function model<Fields extends Model>(fields: Fields){
  return fields;
}

const MyUser = model({
  name: { schema: z.string(), graphqlMapping: "Name" },
  age: { schema: z.number() },
});

const models: Model[] = [MyUser];
for(const model of models){
  for(const [_fieldName, field] of Object.entries(model)){
    field.schema instanceof z.ZodString;
    field.graphqlMapping; // string | undefined
  }
}

As a workaround, you can still use intersection types to add typed metadata to Zod schemas without breaking Zod's compositionality:

type Meta = { graphqlMapping: string };
type AnyObject = z.ZodTypeAny;
type AugmentSchema<T extends z.ZodTypeAny = z.ZodTypeAny> = T & Meta;
function augment<T extends AnyObject>(
  schema: T,
  metadata: Meta
): AugmentSchema<T> {
  (schema as any).meta = metadata;
  return schema as any;
}

const User = z.object({
  name: augment(z.string(), {graphqlMapping:'Name'})
})

User.shape.name.graphqlMapping; // string;

@colinhacks
Copy link
Owner

To answer your original question, there isn't a way to get a serializable representation of an arbitrary Zod schema. You could implement this yourself using the visitor pattern as described here: #335 (comment)

@jasonkuhrt
Copy link
Author

jasonkuhrt commented Jul 5, 2021

Thanks @colinhacks @scotttrinh for the feedback here. Its great to get a deep (and timely!) answer like this!

Points seem reasonable and make sense 👍

@Brian-McBride
Copy link

I know this is closed. But in your last example, does this typing work for the intersection to avoid casting of any?

type Meta = { graphqlMapping: string };
type AnyObject = z.ZodTypeAny & { meta?: Meta };
function augment<T extends AnyObject>(schema: T, metadata: Meta): T {
  schema.meta = metadata;
  return schema;
}

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

No branches or pull requests

4 participants