Skip to content

[Feature Request] Surface Strapi schema constraints in generated types and validation artifacts #55

@WardenCommander

Description

@WardenCommander

[Feature Request] Surface Strapi schema constraints in generated types and validation artifacts

Summary

The generator correctly captures enumerations as TypeScript union types, but silently drops all
other constraint metadata present in the Strapi schema: required, min/max, default,
unique, etc.

This forces consumers to either re-declare these constraints manually or ship without them
entirely — defeating the purpose of a typed, schema-driven client.

There are four distinct gaps, ordered by impact and implementation effort.


Gap 1 — Required fields are not reflected in Input types

Type: Bug / Type correctness

Strapi schemas declare required: true on individual fields. The generator currently emits
every input field as optional and nullable regardless of that flag.

Schema example (reservation.json)

"dateFrom": { "type": "date", "required": true },
"dateTo":   { "type": "date", "required": true },
"note":     { "type": "text" }

Currently generated

export interface ReservationInput {
  dateFrom?: string | null;  // ❌ required in schema — missing field won't be caught by TS
  dateTo?:   string | null;  // ❌ required in schema — missing field won't be caught by TS
  note?:     string | null;  // ✅ correctly optional
}

Expected output

export interface ReservationInput {
  dateFrom: string;          // ✅ non-optional, non-nullable — compile error if omitted
  dateTo:   string;          // ✅ non-optional, non-nullable — compile error if omitted
  note?:    string | null;   // ✅ optional as before
}

Why this matters

The generator already does this correctly for the read typeReservation.dateFrom is
string, not string | null, because the schema marks it required. The Input type should
mirror this symmetry. Currently a developer can write:

await strapi.reservations.create({ data: { note: "hello" } }); // ✅ TypeScript is happy
// ❌ Strapi returns HTTP 400 — dateFrom and dateTo are required

This is a pure TypeScript change — no runtime artifacts needed. The required flag maps
directly to removing ? and | null from the property in the Input interface.

⚠️ Breaking change — this narrows previously-nullable fields in Input types.
Suggest releasing under a minor or major version bump with a migration note.


Gap 2 — Numeric min/max and default values are silently dropped

Type: Enhancement — documentation / tooling

Strapi integer, decimal, and float fields support min, max, and default. These cannot
be expressed in TypeScript's type system natively, but they can be surfaced as
JSDoc annotations at zero runtime cost.

Schema example

"adult":       { "type": "integer", "min": 0,  "default": 0 },
"beforeDays":  { "type": "integer", "min": 0,  "default": 0 },
"pageSize":    { "type": "integer", "min": 1,  "max": 100, "default": 25 },
"commission":  { "type": "float",   "min": 0,  "default": 0 }

Currently generated

adult?:      number | null;
beforeDays?: number | null;
pageSize?:   number | null;
commission?: number | null;

Expected output (with JSDoc)

/** @minimum 0 @default 0 */
adult?: number | null;

/** @minimum 0 @default 0 */
beforeDays?: number | null;

/** @minimum 1 @maximum 100 @default 25 */
pageSize?: number | null;

/** @minimum 0 @default 0 */
commission?: number | null;

Why this matters

JSDoc tags like @minimum, @maximum, and @default are understood by:

  • IDEs (VS Code, WebStorm) — shown on hover
  • ts-to-zod — automatically generates .min() / .max() Zod refinements from JSDoc
  • typescript-json-schema — produces JSON Schema with numeric constraints
  • TypeDoc — renders constraints in generated documentation

This surfaces constraints to the developer at the call site rather than requiring them to
cross-reference the Strapi admin panel or raw schema files.


Gap 3 — Default values not exported as runtime constants

Type: Enhancement — additive export

Strapi schema default values are useful for initialising form state and UI components
client-side. Currently they are not accessible without reading the raw schema JSON.

Proposed addition

A generated *Defaults constant per collection, exported alongside the existing types:

// ✅ Entirely static — derived from schema, zero runtime cost
export const ReservationDefaults = {
  adult:      0,
  beforeDays: 0,
  afterDays:  0,
  parking:    1,
  crib:       0,
  otaExpanse: 0,
  tax:        0,
  commission: 0,
  advance:    0,
  timeFrom:   "14:00:00.000",
  timeTo:     "10:00:00.000",
  state:      "new",
} as const satisfies Partial<ReservationInput>;

The satisfies Partial<ReservationInput> constraint ensures the defaults object stays
in sync with the Input type — a rename or removal will produce a compile error.

Developer experience

// ✅ Before: hard-coded defaults scattered across the codebase
const newReservation: ReservationInput = {
  dateFrom: today,
  dateTo: tomorrow,
  adult: 0,         // where does this 0 come from?
  state: "new",     // magic string
};

// ✅ After: single source of truth from the schema
const newReservation: ReservationInput = {
  ...ReservationDefaults,
  dateFrom: today,
  dateTo: tomorrow,
};

Non-breaking — purely additive new export. Opt-in via a generator flag if bundle size
is a concern.


Gap 4 — No runtime validation schema (Zod / JSON Schema) generated

Type: Enhancement — optional output artifact

JSDoc covers documentation but not runtime validation. Consumers working with forms,
API gateways, or server actions currently have to write Zod schemas by hand that duplicate
what the Strapi schema already defines.

Proposed addition

An optional --validation zod CLI flag (or a separate generated file) that emits a Zod
schema per collection derived directly from the Strapi schema:

// generated/validation.ts  (opt-in, not emitted by default)
import { z } from "zod";

export const ReservationInputSchema = z.object({
  // required: true → non-optional
  dateFrom: z.string(),
  dateTo:   z.string(),

  // optional fields with constraints
  slug:          z.string().optional().nullable(),
  adult:         z.number().int().min(0).default(0).optional().nullable(),
  beforeDays:    z.number().int().min(0).default(0).optional().nullable(),
  parking:       z.number().int().min(0).default(1).optional().nullable(),
  commission:    z.number().min(0).default(0).optional().nullable(),
  securityDeposit: z.number().int().optional().nullable(),

  // enumerations
  state: z.enum(["inquiry", "new", "modified", "cancelled"]).default("new").optional().nullable(),

  // relations — accept StrapiID or RelationOperations
  units:    z.union([z.string(), z.number(), z.array(z.union([z.string(), z.number()])), z.object({ connect: z.array(z.any()).optional(), disconnect: z.array(z.any()).optional(), set: z.array(z.any()).optional() })]).optional().nullable(),
});

// Type alias derived from the schema — ReservationInput stays the single source of truth
export type ReservationInput = z.infer<typeof ReservationInputSchema>;

Strapi schema → Zod mapping table

Strapi attribute Zod output
required: true .nonoptional() (no ?)
type: "integer" z.number().int()
type: "float" / "decimal" z.number()
type: "string" / "text" / "uid" z.string()
type: "boolean" z.boolean()
type: "date" / "datetime" z.string() (ISO 8601)
type: "enumeration" z.enum([...values])
min: N .min(N)
max: N .max(N)
default: value .default(value)
type: "relation" RelationInputSchema (shared helper)
type: "media" (single) z.union([z.string(), z.number()])
type: "media" (multiple) z.array(z.union([z.string(), z.number()]))

Why this matters

  • Forms — pass directly to react-hook-form resolver (zodResolver(ReservationInputSchema))
  • Server actions — validate incoming payloads before hitting Strapi
  • API gateways — validate at the edge without importing Strapi itself
  • Single source of truth — constraints live in the Strapi schema, not duplicated in
    application code

Non-breaking — purely additive, opt-in via CLI flag. Requires zod as a peer dependency
only when the flag is used.


Implementation priority

# Gap Breaking Effort Value
1 Required fields in Input types ⚠️ Yes Low 🔴 High — prevents silent HTTP 400s
2 JSDoc @minimum / @maximum / @default No Low 🟡 Medium — improves tooling
3 *Defaults constants No Low 🟡 Medium — improves DX
4 Zod schema generation (--validation zod) No Medium 🟠 High for form-heavy consumers

Gap 1 is the highest-value fix: it catches missing required fields at compile time
rather than at runtime when Strapi returns a 400 Bad Request. Gaps 2–4 are fully
additive and can ship independently.


Additional context

This gap was discovered while using the generated client with a React Native / Expo app
and a Next.js portal that both use react-hook-form + Zod for form validation. Every
content-type had to maintain a parallel hand-written Zod schema to enforce the same
constraints that the Strapi schema already defines — creating two sources of truth that
drift over time whenever the schema changes.

The attached types file (types.ts) and schema (reservation.json) are from a real
production Strapi v5 instance and were used as the basis for the examples above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions