Skip to main content
Without minItems and maxItems, the model decides how many array elements to produce. In practice, this means some requests return zero items (breaking downstream consumers that expect at least one) and others return dozens (blowing up storage, UI layouts, or token budgets). Array bounds make the contract explicit: you get at least N and at most M items, every time. Item-level constraints matter too. An unbounded array of unconstrained strings is effectively uncontrolled output. Adding pattern and minLength/maxLength to the items turns the array into a well-defined, predictable structure.

Use case

Generating article tags for search indexing. You need 3-8 tags, each short and lowercase, suitable for a tag-based search index.

Schema pattern

{
  "type": "object",
  "properties": {
    "tags": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^[a-z0-9-]{2,24}$"
      },
      "minItems": 3,
      "maxItems": 8
    }
  },
  "required": ["tags"],
  "additionalProperties": false
}
from pydantic import BaseModel, ConfigDict, Field, model_validator

class TagsPayload(BaseModel):
    model_config = ConfigDict(extra="forbid")
    tags: list[str] = Field(..., min_length=3, max_length=8)

    @model_validator(mode="after")
    def validate_tags(self):
        if len(set(self.tags)) != len(self.tags):
            raise ValueError("tags must be unique")
        for tag in self.tags:
            if len(tag) < 2 or len(tag) > 24:
                raise ValueError("tag length must be 2-24")
            import re
            if re.match(r"^[a-z0-9-]{2,24}$", tag) is None:
                raise ValueError("tag must match slug pattern")
        return self
import { z } from "zod";

const tagsPayloadSchema = z.object({
  tags: z.array(z.string().regex(/^[a-z0-9-]{2,24}$/)).min(3).max(8),
}).strict().superRefine((data, ctx) => {
  if (new Set(data.tags).size !== data.tags.length) {
    ctx.addIssue({ code: z.ZodIssueCode.custom, message: "tags must be unique" });
  }
});

Example output

{
  "tags": [
    "password-reset",
    "mobile-app",
    "ios",
    "auth-flow"
  ]
}

Why this works

minItems: 3 guarantees you always have enough tags for meaningful search indexing. maxItems: 8 caps generation cost and keeps tag lists manageable in UI rendering. The item-level pattern: "^[a-z0-9-]{2,24}$" enforces a slug format: lowercase, hyphenated, with no spaces. That means tags are already normalized for your search index without any post-processing.