t3-env icon indicating copy to clipboard operation
t3-env copied to clipboard

Default values skipped when using SKIP_ENV_VALIDATION

Open iFlyinq opened this issue 1 year ago • 2 comments
trafficstars

Description

When using SKIP_ENV_VALIDATION=true with @t3-oss/env-nextjs, the entire Zod validation process is skipped. This is useful for scenarios like Docker builds, but it introduces an unexpected side effect: default values specified in the Zod schema are not applied.

This behavior can lead to undefined environment variables where default values were expected, potentially causing buildtime errors or unexpected behavior in the application.

Current Behavior

  1. When SKIP_ENV_VALIDATION=true, all Zod validations are bypassed.
  2. Default values specified in the Zod schema (e.g., .default("development")) are not applied.
  3. Environment variables without a value remain undefined instead of using their specified defaults.

Expected Behavior

Even when skipping full validation:

  1. Default values specified in the Zod schema should be applied.
  2. Basic type coercion (e.g., z.coerce.number()) should still occur.
  3. The application should be able to run with a minimal set of required environment variables.

iFlyinq avatar Sep 12 '24 20:09 iFlyinq

Hey!

I just released a library for this issue - it's a drop-in replacement for the one you're currently using, with some extra features like a built-in CLI and live preview:

🔗 https://envin.turbostarter.dev/

Feel free to reach out if you have any questions or suggestions - every bit of feedback is welcome! 🔥

Bartek532 avatar Jun 15 '25 17:06 Bartek532

If anyone is looking for a solution. This is how I solved it for now. It finds and extracts defaults for undefined variables and applies transformation.

//utils.ts
import {
  type ClientSchemaType,
  type ParsedEnvType,
  type ServerSchemaType,
} from '~/env/env';

export function finishEnv(data: {
  intermediateEnv: ParsedEnvType;
  skipValidation: boolean;
  clientSchema: ClientSchemaType;
  serverSchema: ServerSchemaType;
}) {
  const { intermediateEnv, skipValidation, clientSchema, serverSchema } = data;
  const env = { ...intermediateEnv };

  if (skipValidation) {
    const defaults = {
      ...extractDefaults(serverSchema),
      ...extractDefaults(clientSchema),
    };

    for (const [key, defaultValue] of Object.entries(defaults)) {
      if ((env as any)[key] === undefined) {
        (env as any)[key] = defaultValue;
      } else {
        const schema = (serverSchema as any)[key] || (clientSchema as any)[key];
        if (schema && typeof (env as any)[key] === 'string') {
          try {
            const transformed = schema.safeParse((env as any)[key]);
            if (transformed.success) {
              (env as any)[key] = transformed.data;
            }
          } catch (error) {}
        }
      }
    }
  }

  return env;
}

function extractDefaults(schema: ServerSchemaType | ClientSchemaType) {
  const defaults: Record<string, any> = {};
  for (const [key, zodSchema] of Object.entries(schema)) {
    try {
      const undefinedResult = zodSchema.safeParse(undefined);
      if (undefinedResult.success) {
        defaults[key] = undefinedResult.data;
      } else {
        const envValue = process.env[key];
        if (envValue !== undefined) {
          const result = zodSchema.safeParse(envValue);
          if (result.success) {
            defaults[key] = result.data;
          }
        }
      }
    } catch (error) {}
  }
  return defaults;
}
//env.ts
import { createEnv } from '@t3-oss/env-nextjs';

import { z } from 'zod';
import { orgClientSchema, orgRuntimeEnv, orgServerSchema } from './org';
import { finishEnv } from './utils';

const skipValidation = !!process.env.SKIP_ENV_VALIDATION;
/**
 * Specify your server-side environment variables schema here. This way you can ensure the app
 * isn't built with invalid env vars.
 */
const serverSchema = {
  ...orgServerSchema,

  REST_API_SERVICE_URL: z.string().url(),

  LOG_API_SERVICE_ERRORS: z
    .enum(['true', 'false'])
    .optional()
    .transform((v) => (v === undefined ? undefined : v === 'true')),
};
export type ServerSchemaType = typeof serverSchema;

/**
 * Specify your client-side environment variables schema here. This way you can ensure the app
 * isn't built with invalid env vars. To expose them to the client, prefix them with
 * `NEXT_PUBLIC_`.
 * Warning: NEXT_PUBLIC_ variables are inlined(baked in) during the build process only! They cannot be set during runtime.
 * @see https://nextjs.org/docs/app/guides/environment-variables#runtime-environment-variables
 */
const clientSchema = {
  ...orgClientSchema,
};
export type ClientSchemaType = typeof clientSchema;

const intermediateEnv = createEnv({
  server: serverSchema,
  client: clientSchema,

  /**
   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
   * middlewares) or client-side so we need to destruct manually.
   */
  runtimeEnv: {
    ...orgRuntimeEnv,
    REST_API_SERVICE_URL: process.env.REST_API_SERVICE_URL,
    LOG_API_SERVICE_ERRORS: process.env.LOG_API_SERVICE_ERRORS,
  },

  /**
   * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
   * useful for Docker builds.
   */
  skipValidation,
  /**
   * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
   * `SOME_VAR=''` will throw an error.
   */
  emptyStringAsUndefined: true,
});
export type ParsedEnvType = typeof intermediateEnv;

export const finishedEnv = finishEnv({
  intermediateEnv,
  skipValidation,
  clientSchema,
  serverSchema,
});
//index.ts
export { finishedEnv as env } from './env';

BlvckParrot avatar Jul 23 '25 10:07 BlvckParrot