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

Strict by default or strict deep #2062

Open
hornta opened this issue Feb 19, 2023 · 6 comments
Open

Strict by default or strict deep #2062

hornta opened this issue Feb 19, 2023 · 6 comments
Labels
enhancement New feature or request

Comments

@hornta
Copy link

hornta commented Feb 19, 2023

Hi, I would like to either have strict on all of my types or something like .deepStrict() that propagates down on all sub objects in my schema. Now it's too easy to miss out on defining a schema as strict I think.

@JacobWeisenburger JacobWeisenburger added the enhancement New feature or request label Feb 19, 2023
@gawi
Copy link

gawi commented Feb 21, 2023

I also think it would be worthwhile to have this in the API (and possibly deepPassthrough() and deepStrip()).

If you're in a hurry, I think you can implement it yourself. Take a look at deepPartial+deepPartialify for inspiration.

  deepPartial(): partialUtil.DeepPartial<this> {
    return deepPartialify(this) as any;
  }
function deepPartialify(schema: ZodTypeAny): any {
  if (schema instanceof ZodObject) {
    const newShape: any = {};

    for (const key in schema.shape) {
      const fieldSchema = schema.shape[key];
      newShape[key] = ZodOptional.create(deepPartialify(fieldSchema));
    }
    return new ZodObject({
      ...schema._def,
      shape: () => newShape,
    }) as any;
  } else if (schema instanceof ZodArray) {
    return ZodArray.create(deepPartialify(schema.element));
  } else if (schema instanceof ZodOptional) {
    return ZodOptional.create(deepPartialify(schema.unwrap()));
  } else if (schema instanceof ZodNullable) {
    return ZodNullable.create(deepPartialify(schema.unwrap()));
  } else if (schema instanceof ZodTuple) {
    return ZodTuple.create(
      schema.items.map((item: any) => deepPartialify(item))
    );
  } else {
    return schema;
  }
}

@gawi
Copy link

gawi commented Feb 22, 2023

I've tried to generalize to all kind of deep unknown policies. Here's what I got:

First, you need 2 utilities: a function and a type.

type ZodObjectMapper<T extends ZodRawShape, U extends UnknownKeysParam> = (
  o: ZodObject<T>
) => ZodObject<T, U>;

function deepApplyObject(
  schema: ZodTypeAny,
  map: ZodObjectMapper<any, any>
): any {
  if (schema instanceof ZodObject) {
    const newShape: Record<string, ZodTypeAny> = {};
    for (const key in schema.shape) {
      const fieldSchema = schema.shape[key];
      newShape[key] = deepApplyObject(fieldSchema, map);
    }
    const newObject = new ZodObject({
      ...schema._def,
      shape: () => newShape,
    });
    return map(newObject);
  } else if (schema instanceof ZodArray) {
    return ZodArray.create(deepApplyObject(schema.element, map));
  } else if (schema instanceof ZodOptional) {
    return ZodOptional.create(deepApplyObject(schema.unwrap(), map));
  } else if (schema instanceof ZodNullable) {
    return ZodNullable.create(deepApplyObject(schema.unwrap(), map));
  } else if (schema instanceof ZodTuple) {
    return ZodTuple.create(
      schema.items.map((item: any) => deepApplyObject(item, map))
    );
  } else {
    return schema;
  }
}
type DeepUnknownKeys<
  T extends ZodTypeAny,
  UnknownKeys extends UnknownKeysParam
> = T extends ZodObject<infer Shape, infer _, infer Catchall>
  ? ZodObject<
      {
        [k in keyof Shape]: DeepUnknownKeys<Shape[k], UnknownKeys>;
      },
      UnknownKeys,
      Catchall
    >
  : T extends ZodArray<infer Type, infer Card>
  ? ZodArray<DeepUnknownKeys<Type, UnknownKeys>, Card>
  : T extends ZodOptional<infer Type>
  ? ZodOptional<DeepUnknownKeys<Type, UnknownKeys>>
  : T extends ZodNullable<infer Type>
  ? ZodNullable<DeepUnknownKeys<Type, UnknownKeys>>
  : T extends ZodTuple<infer Items>
  ? {
      [k in keyof Items]: Items[k] extends ZodTypeAny
        ? DeepUnknownKeys<Items[k], UnknownKeys>
        : never;
    } extends infer PI
    ? PI extends ZodTupleItems
      ? ZodTuple<PI>
      : never
    : never
  : T;

Then we only need to define those 3 functions + types:

type DeepPassthrough<T extends ZodTypeAny> = DeepUnknownKeys<T, 'passthrough'>;
function deepPassthrough<T extends ZodTypeAny>(schema: T): DeepPassthrough<T> {
  return deepApplyObject(schema, (s) => s.passthrough()) as DeepPassthrough<T>;
}

type DeepStrip<T extends ZodTypeAny> = DeepUnknownKeys<T, 'strip'>;
function deepStrip<T extends ZodTypeAny>(schema: T): DeepStrip<T> {
  return deepApplyObject(schema, (s) => s.strip()) as DeepStrip<T>;
}

type DeepStrict<T extends ZodTypeAny> = DeepUnknownKeys<T, 'strict'>;
function deepStrict<T extends ZodTypeAny>(
  schema: T,
  error?: errorUtil.ErrMessage
): DeepStrict<T> {
  return deepApplyObject(schema, (s) => s.strict(error)) as DeepStrict<T>;
}

Note that I've left some any in the definitions above. Open to any suggestion to fix this.

And that's it. Sample usage:

const schema = z.object({
  a: z.string(),
  b: z.object({
    a: z.string(),
    b: z.array(
      z.object({
        a: z.string(),
      })
    ),
  }),
});

const value = {
  a: 'value',
  b: {
    a: 'value',
    b: [{ a: 'value' }, { a: 'value', c: 'unknown' }],
    c: 'unknown',
  },
  c: 'unknown',
};

const stripSchema = deepStrip(schema);
console.log(JSON.stringify(stripSchema.parse(value)));
// => {"a":"value","b":{"a":"value","b":[{"a":"value"},{"a":"value"}]}}

const passthroughSchema = deepPassthrough(schema);
console.log(JSON.stringify(passthroughSchema.parse(value)));
// => {"a":"value","b":{"a":"value","b":[{"a":"value"},{"a":"value","c":"unknown"}],"c":"unknown"},"c":"unknown"}

const strictSchema = deepStrict(schema, 'ERROR');
console.log(JSON.stringify(strictSchema.parse(value)));
// => throws

@slapierre
Copy link

slapierre commented Feb 22, 2023

Caveat: the implementation provided by @gawi suffers the same shortcomings as deepPartial:

Important limitation: deep partials only work as expected in hierarchies of objects, arrays, and tuples.

The policy of a recursive type (defined with z.lazy) cannot be changed with the current implemetation of deepApplyObject, it would need to be enhanced to handle a schema of type ZodType<T, ZodTypeDef, U>, it doesn't look like a trivial change.

@colinhacks
Copy link
Owner

colinhacks commented Feb 26, 2023

deepPartial is very fragile and probably shouldn't have been added.

Zod intentionally avoids "modes" and contextual validation - it's too hard to reason about. Using z.strictObject everywhere is the recommended approach. You might be able to make a custom lint rule to prevent any usage of z.object. If you do that, ping me and I'll add it to the readme so others can use it too.

@hyperscientist
Copy link

For consuming APIs I would almost always pick z.object, but for data exploration as I am doing now z.strictObject is necessary and I am glad I found about it. Of course if you want highly focused lib (on consuing known APIs) with great DX than it may not make sense to add it. Just sharing my pov.

@alokmenghrajani
Copy link

TypeScript's structural-based type system makes it easy to accidentally leak data (e.g. include password hashes in an HTTP response after fetching a User row from a database table) when building web applications. Thanks to this issue, I know about z.object().strict()/z.strictObject() and strict parsing is a great way to work around some of these risks: I can parse responses prior to returning them to the user and fail if there are extra fields.

Defining all the objects to be .strictObject is not ideal in some circumstances. It can make changes hard. For instance, a web client + web server pair need to be deployed at the exact same time if they are sharing their zod schema. Alternatively, the schema's new fields need to first be made optional, requiring multiple deploys to reach the end state.

I'm therefore writing this comment to +1 the idea of adding .deepStrict() or having a strictParse() which behaves like parse() but considers object to be strictObject. Backend could expose non-strict schemas but use strict schemas to validate data prior to sending responses. In a similar way, frontends could implement strict schema parsing prior to making a request but the server can accept additional fields.

P.S. I'm a long time zod fan, first time sharing some feedback -- thanks a lot of writing and maintaining this high quality library over the years!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants