Skip to main content
Users type addresses with inconsistent formatting, phone numbers with or without country codes, and shipping preferences in plain English. If you pass this text to your backend as-is, you need normalization logic, validation logic, and error handling for every possible format variation. Schema-constrained generation does this normalization at extraction time: the model reads the messy input and produces a clean, typed object that conforms to your backend’s expectations.

Goal

Convert mixed-quality user submissions into a normalized order payload with correct types, validated formats, and consistent structure, ready for your order creation API.

Schema contract

{
  "type": "object",
  "properties": {
    "customer": {
      "type": "object",
      "properties": {
        "name": { "type": "string", "minLength": 1, "maxLength": 100 },
        "email": { "type": "string", "format": "email" },
        "phone": { "type": ["string", "null"], "pattern": "^\\+?[1-9][0-9]{7,14}$" }
      },
      "required": ["name", "email", "phone"],
      "additionalProperties": false
    },
    "shipping": {
      "type": "object",
      "properties": {
        "method": { "type": "string", "enum": ["standard", "express"] },
        "address_line1": { "type": "string", "minLength": 1, "maxLength": 120 },
        "city": { "type": "string", "minLength": 1, "maxLength": 80 },
        "postal_code": { "type": "string", "minLength": 3, "maxLength": 20 },
        "country": { "type": "string", "pattern": "^[A-Z]{2}$" }
      },
      "required": ["method", "address_line1", "city", "postal_code", "country"],
      "additionalProperties": false
    },
    "notes": { "type": ["string", "null"], "maxLength": 300 }
  },
  "required": ["customer", "shipping"],
  "additionalProperties": false
}
from typing import Literal
from pydantic import BaseModel, ConfigDict, EmailStr, Field

class Customer(BaseModel):
    model_config = ConfigDict(extra="forbid")
    name: str = Field(..., min_length=1, max_length=100)
    email: EmailStr
    phone: str | None = Field(..., pattern=r"^\+?[1-9][0-9]{7,14}$")

class Shipping(BaseModel):
    model_config = ConfigDict(extra="forbid")
    method: Literal["standard", "express"]
    address_line1: str = Field(..., min_length=1, max_length=120)
    city: str = Field(..., min_length=1, max_length=80)
    postal_code: str = Field(..., min_length=3, max_length=20)
    country: str = Field(..., pattern=r"^[A-Z]{2}$")

class OrderPayload(BaseModel):
    model_config = ConfigDict(extra="forbid")
    customer: Customer
    shipping: Shipping
    notes: str | None = Field(None, max_length=300)
import { z } from "zod";

const orderPayloadSchema = z.object({
  customer: z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    phone: z.string().regex(/^\+?[1-9][0-9]{7,14}$/).nullable(),
  }).strict(),
  shipping: z.object({
    method: z.enum(["standard", "express"]),
    address_line1: z.string().min(1).max(120),
    city: z.string().min(1).max(80),
    postal_code: z.string().min(3).max(20),
    country: z.string().regex(/^[A-Z]{2}$/),
  }).strict(),
  notes: z.string().max(300).nullable().optional(),
}).strict();
curl https://api.dottxt.ai/v1/chat/completions \
  -H "Authorization: Bearer $DOTTXT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-oss-20b",
    "messages": [{ "role": "user", "content": "Ship this to 10 Main Street, Austin 78701 US. Use express please. Name: Alice Johnson, email alice@acme.com. No phone." }],
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "order_payload",
        "schema": {
          "type": "object",
          "properties": {
            "customer": {
              "type": "object",
              "properties": {
                "name": { "type": "string", "minLength": 1, "maxLength": 100 },
                "email": { "type": "string", "format": "email" },
                "phone": { "type": ["string", "null"], "pattern": "^\\+?[1-9][0-9]{7,14}$" }
              },
              "required": ["name", "email", "phone"],
              "additionalProperties": false
            },
            "shipping": {
              "type": "object",
              "properties": {
                "method": { "type": "string", "enum": ["standard", "express"] },
                "address_line1": { "type": "string", "minLength": 1, "maxLength": 120 },
                "city": { "type": "string", "minLength": 1, "maxLength": 80 },
                "postal_code": { "type": "string", "minLength": 3, "maxLength": 20 },
                "country": { "type": "string", "pattern": "^[A-Z]{2}$" }
              },
              "required": ["method", "address_line1", "city", "postal_code", "country"],
              "additionalProperties": false
            },
            "notes": { "type": ["string", "null"], "maxLength": 300 }
          },
          "required": ["customer", "shipping"],
          "additionalProperties": false
        }
      }
    }
  }'

Example input

Ship this to 10 Main Street, Austin 78701 US.
Use express please.
I'm Alice Johnson, email alice@acme.com. No phone.

Example output

{
  "customer": {
    "name": "Alice Johnson",
    "email": "alice@acme.com",
    "phone": null
  },
  "shipping": {
    "method": "express",
    "address_line1": "10 Main Street",
    "city": "Austin",
    "postal_code": "78701",
    "country": "US"
  }
}

Implementation tips

  • Nullable for “asked but absent.” The user explicitly said “no phone,” so phone is null rather than omitted. Your backend can distinguish “no phone provided” from “phone not asked,” which is useful for follow-up workflows.
  • Enums for controlled vocabulary. method is "standard" or "express", not "fast", "next day", or "ASAP". The enum forces normalization so your shipping logic doesn’t need string matching.
  • Pattern for format enforcement. The country field uses "^[A-Z]{2}$" so the model produces "US" instead of "United States", "usa", or "U.S.A.". The same idea applies to phone: the E.164-like pattern ensures a format your telephony API accepts.
  • additionalProperties: false prevents the model from forwarding raw user text as extra fields, which could leak PII into systems that don’t expect it.