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

Cross-field validation #19

Open
iainsproat opened this issue Mar 6, 2025 · 1 comment
Open

Cross-field validation #19

iainsproat opened this issue Mar 6, 2025 · 1 comment
Labels
good first issue Good for newcomers help wanted Extra attention is needed

Comments

@iainsproat
Copy link

Given the following schema, I wish for the validation of FLAG_B_DEPENDS_ON_FLAG_A to depend on the value of FLAG_A:

parseEnv(process.env,
  {
    FLAG_A: {
      schema: z.boolean(),
      defaults: { _: false }
    },
    FLAG_B_DEPENDS_ON_FLAG_A: {
      schema: z.boolean(),
      defaults: { _: false }
    }
  })

In Zod cross-field validation can be achieved via .refine on the object, e.g.:

z.object(
  {
    FLAG_A: {
      schema: z.boolean(),
      defaults: { _: false }
    },
    FLAG_B_DEPENDS_ON_FLAG_A: {
      schema: z.boolean(),
      defaults: { _: false }
    }
  }).refine({ FLAG_A, FLAG_B_DEPENDS_ON_FLAG_A }=> {
      if (FLAG_B_DEPENDS_ON_FLAG_A && !FLAG_A) {
        return false
      }

      return true;
    },
    () => ({
      path: ['FLAG_A'],
      message: '`FLAG_A` field is required to be true if `FLAG_B_DEPENDS_ON_FLAG_A` is true',
    })
  );

How might this be achieved with znv?

@lostfictions
Copy link
Owner

lostfictions commented Mar 24, 2025

Hmm... personally for these sorts of cases I think the best answer is often to encode this as an enum or a discriminated union.

In this particular case, you're describing three possible states: nothing, A, A and B. With Zod and TypeScript, this is easy to express in a type-safe way using enums:

export const { MY_MODE } = parseEnv(process.env, {
  MY_MODE: z.enum(["none", "flag_a", "flag_a_and_flag_b"]).default("none"),
});

If there's more complex state attached to each flag, this could potentially be expressed as a discriminated union instead.

Incidentally, I think this is a good rule of thumb not just for configuration but also for structuring code internally -- a union type of literals can express these constraints more accurately than two booleans can, because you can't constrain the two booleans to exclude the fourth, invalid state (B and not A) without scattering imperative logic around to check it.

As a side note, twelve factor best practices suggest that env vars should be granular and orthogonal to each other, so there's another reason to avoid this sort of interdependency across vars.

That said, I get that the real world can be messy sometimes, so I'd be open to hear out alternate solutions to this if someone can present a solid case where enums/discriminated unions might not be the best solution, and can sketch an unobtrusive API design for expressing this sort of interdependency in a type-safe way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants