# Zod Schema Patterns and Best Practices in Read Frog's Configuration System

> Discover Zod schema patterns and best practices in Read Frog's configuration system. Learn about JIT-less initialization, discriminated unions, constant validation, and superRefine for type safety and extensibility.

- Repository: [MengXi/read-frog](https://github.com/mengxi-ream/read-frog)
- Tags: best-practices
- Published: 2026-03-07

---

**Read Frog leverages JIT-less Zod initialization, discriminated unions for heterogeneous provider configurations, constant-driven validation ranges, and superRefine for cross-field business logic to maintain a type-safe, extensible configuration system for its Chrome extension.**

Read Frog is an open-source Chrome extension that manages complex user settings across AI translation providers, subtitles, and UI components. The project implements sophisticated Zod schema patterns to validate runtime configuration while deriving static TypeScript types from a single source of truth. This architectural approach ensures strict Content Security Policy (CSP) compliance in Manifest V3 environments without sacrificing developer experience or validation accuracy.

## JIT-Less Initialization for Manifest V3 Compliance

Chrome extensions running Manifest V3 enforce strict CSP rules that prohibit dynamic code evaluation via `new Function()`. Since Zod's default just-in-time (JIT) compilation uses `new Function()` for optimized validation, Read Frog explicitly disables this feature to avoid runtime errors.

In [`src/utils/zod-config.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/zod-config.ts), the codebase initializes Zod with the `jitless` flag before any schemas are defined:

```typescript
// src/utils/zod-config.ts
import { z } from "zod"

// Disable Zod JIT (new Function) to avoid CSP eval violation in MV3 extensions
// https://github.com/colinhacks/zod/issues/4360
z.config({ jitless: true })

```

This pattern guarantees that all subsequent schema validations in `src/types/config/` execute safely within the extension's sandboxed environment.

## Hierarchical Schema Composition

The configuration system uses nested `z.object` schemas to mirror the logical grouping of application settings. The root schema in [`src/types/config/config.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/types/config/config.ts) composes smaller, dedicated schemas for each feature domain:

```typescript
// src/types/config/config.ts
export const configSchema = z.object({
  language: languageSchema,
  providersConfig: providersConfigSchema,
  translate: translateConfigSchema,
  tts: ttsConfigSchema,
  floatingButton: floatingButtonSchema,
  selectionToolbar: selectionToolbarSchema,
  videoSubtitles: videoSubtitlesSchema,
  // ... additional feature sections
})
.superRefine((data, ctx) => {
  // Cross-field validation: ensure selected provider IDs exist & are enabled
  // ...
})

```

This modularity allows individual features to evolve independently while maintaining a cohesive validation surface at the configuration root.

## Discriminated Unions for Provider Polymorphism

Read Frog supports multiple AI providers with distinct configuration shapes. Rather than using optional fields on a monolithic object, the codebase employs `z.discriminatedUnion` to preserve type discrimination across heterogeneous provider configurations.

In [`src/types/config/provider.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/types/config/provider.ts), the `providerConfigItemSchema` uses a discriminator key to distinguish between provider types:

```typescript
// src/types/config/provider.ts
export const providerConfigItemSchema = z.discriminatedUnion(
  "provider",
  providerConfigSchemaList
)

export const providersConfigSchema = z.array(providerConfigItemSchema)
.superRefine((providers, ctx) => {
  // Enforce unique `id` and `name` across the array
  // ...
})

```

Each object in the `providers` array must include a `provider` literal field, enabling TypeScript to narrow the exact shape (model lists, API keys, endpoints) based on the specific provider type.

## Constant-Driven Validation and Reusable Sub-Schemas

All numeric limits and fixed options are centralized in `src/utils/constants/` and imported into schemas, ensuring validation rules remain synchronized with application constraints. This pattern prevents magic numbers and allows range adjustments without modifying validation logic.

The translation configuration in [`src/types/config/translate.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/types/config/translate.ts) imports minimum values for queue configuration:

```typescript
// src/types/config/translate.ts
import { MIN_TRANSLATE_CAPACITY, MIN_TRANSLATE_RATE } from "@/utils/constants"

export const requestQueueConfigSchema = z.object({
  capacity: z.number().gte(MIN_TRANSLATE_CAPACITY),
  rate: z.number().gte(MIN_TRANSLATE_RATE),
})

export const batchQueueConfigSchema = z.object({
  maxCharactersPerBatch: z.number().gte(MIN_BATCH_CHARACTERS),
  maxItemsPerBatch: z.number().gte(MIN_BATCH_ITEMS),
})

```

Both translation and subtitle modules reuse these queue schemas, eliminating duplication while guaranteeing identical validation behavior across features.

## Cross-Field Validation with superRefine

Simple field-level validation cannot detect logical errors spanning multiple properties. Read Frog implements `superRefine` for complex business rules such as duplicate ID detection, prompt reference validation, and provider-feature compatibility.

In [`src/types/config/translate.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/types/config/translate.ts), the custom prompts schema validates that `promptId` references an existing pattern:

```typescript
// src/types/config/translate.ts
export const customPromptsConfigSchema = z.object({
  promptId: z.string().nullable(),
  patterns: z.array(patternSchema),
})
.superRefine((data, ctx) => {
  if (data.promptId !== null) {
    const patternIds = data.patterns.map(p => p.id)
    if (!patternIds.includes(data.promptId)) {
      ctx.addIssue({
        code: "invalid_value",
        values: patternIds,
        message: `promptId "${data.promptId}" must be null or match a pattern id`,
        path: ["promptId"],
      })
    }
  }
})

```

Similarly, [`src/types/config/provider.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/types/config/provider.ts) uses `superRefine` to enforce unique `id` and `name` values across the providers array, preventing duplicate configurations at runtime.

## Type Inference as Single Source of Truth

Every schema file exports an inferred TypeScript type using `z.infer`, eliminating the need for parallel manual type definitions. This pattern ensures compile-time types and runtime validators remain synchronized automatically.

From [`src/types/config/translate.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/types/config/translate.ts):

```typescript
// src/types/config/translate.ts
export type TranslateConfig = z.infer<typeof translateConfigSchema>

```

Consumers import `TranslateConfig` for static typing while the `configSchema.parse()` method guarantees runtime safety, creating a single source of truth for configuration shape.

## Practical Implementation Examples

### Validating Runtime Configuration

To validate configuration loaded from Chrome storage:

```typescript
import { configSchema } from "@/types/config/config"
import { z } from "zod"

async function validateStoredConfig(raw: unknown) {
  try {
    const parsed = configSchema.parse(raw) // Throws ZodError on failure
    return parsed
  } catch (e) {
    if (e instanceof z.ZodError) {
      console.error("Validation errors:", e.format())
    }
    throw e
  }
}

```

### Adding a New Provider

Extending the system to support a new translation provider requires minimal changes:

```typescript
// src/types/config/provider.ts
export const providerConfigSchemaList = [
  ...existingProviderSchemas,
  baseAPIProviderConfigSchema.extend({
    provider: z.literal("new-provider"),
    apiKey: z.string().min(1),
    endpoint: z.string().url(),
  }),
]

```

The discriminated union automatically incorporates the new shape without modifying the root `configSchema`.

## Summary

- **JIT-less initialization** in [`src/utils/zod-config.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/zod-config.ts) ensures CSP compliance for Chrome Manifest V3 extensions by disabling Zod's `new Function` optimization.
- **Discriminated unions** handle polymorphic provider configurations while preserving TypeScript type narrowing through literal discriminator fields.
- **Constant-driven validation** centralizes numeric limits and enum values in `src/utils/constants/`, preventing drift between validation rules and application constraints.
- **Reusable sub-schemas** for common structures like request queues eliminate duplication across translation and subtitle modules.
- **superRefine** implements complex cross-field validation for duplicate IDs, prompt references, and provider-feature compatibility that single-field validators cannot express.
- **Type inference** via `z.infer` creates a single source of truth, exporting TypeScript types directly from Zod schemas to eliminate manual type maintenance.

## Frequently Asked Questions

### Why does Read Frog disable Zod's JIT compiler?

Read Frog disables JIT compilation via `z.config({ jitless: true })` to comply with Chrome Extension Manifest V3 Content Security Policies. The default JIT mode uses `new Function()` for performance optimization, which triggers CSP violations in sandboxed extension environments. The JIT-less mode ensures validation logic executes safely without dynamic code evaluation.

### How does the provider configuration handle different API shapes?

The system uses `z.discriminatedUnion` with a `provider` literal field to distinguish between configuration types. Each provider variant extends a base schema with provider-specific fields (API keys, model lists, endpoints). TypeScript automatically narrows the type when accessing provider properties, ensuring type-safe access to fields that only exist on specific provider configurations.

### What pattern ensures promptId references valid custom patterns?

The `customPromptsConfigSchema` implements `superRefine` to verify that the `promptId` field either equals `null` or matches an `id` present in the `patterns` array. This cross-field validation runs after initial type checking, adding a business logic constraint that prevents orphaned prompt references in the configuration object.

### How are numeric limits centralized across the configuration?

All validation constants reside in `src/utils/constants/` and are imported into schema definitions (e.g., `MIN_TRANSLATE_CAPACITY`, `MIN_BATCH_CHARACTERS`). This separation allows developers to adjust limits in one location, automatically propagating changes to both validation logic and any dependent application code referencing the same constants.