Standard Schema

Une entité est un Standard Schema. C'est la plus petite façon d'utiliser Fougere : un paquet, une classe, à l'intérieur du framework que vous avez déjà.

import { entity, primary, text, bool } from '@fougere/schema';

export default class Post extends entity({
  id: primary(),
  title: text({ min: 1, max: 200 }),
  draft: bool({ default: false }),
}) {}

Post['~standard'];
// → { version: 1, vendor: 'fougere', validate }

~standard est un getter statique sur la classe : Post est le schéma lui-même — rien à envelopper, aucun paquet d'adaptation à installer.

Où elle est acceptée

Toute bibliothèque qui lit la spec prend une entité directement : tRPC, Hono, TanStack Form, et — annoncé pour NestJS 12 — un Standard Schema dans @Body, @Query et @Param, en successeur de class-validator.

Les dérivations portent l'interface elles aussi, et c'est ce qui la rend utile à une frontière de route — la forme qu'un appelant peut proposer est rarement la ligne entière :

export class PostDraft extends Post.pick('title') {}
export class PostPatch extends Post.omit('id').partial() {}

PostDraft['~standard'].validate({ title: 'Bonjour' });  // → { value: { title: 'Bonjour' } }

pick, omit, partial et extend produisent toutes une classe qui est elle-même un Standard Schema. Voir Vues.

La forme du résultat

La validation est synchrone — la spec autorise une promesse, ce vendor n'en renvoie jamais.

const result = Post['~standard'].validate({ id: 'abc', title: '' });

if (result.issues) {
  result.issues[0].message;  // 'String is too short (0 < 1).'
  result.issues[0].path;     // [{ key: 'title' }]
} else {
  result.value;              // la ligne jugée et décodée
}

path est une liste de segments { key }, conformément à la spec. Une erreur portant sur l'entrée dans son ensemble — un null, un non-objet — ne porte aucun path, plutôt qu'un path vide.

La value est la ligne décodée, pas l'entrée brute : un champ qui déclare une Date revient en Date, parce que l'axe boundary applique sa conversion entrante dans le cadre du jugement.

Les valeurs par défaut ne sont pas remplies

Un comportement diffère de la plupart des vendors, et c'est la doctrine, pas un oubli.

const { value } = Post['~standard'].validate({ id: 'abc', title: 'Bonjour' });
'draft' in value;  // false — alors que draft déclare { default: false }

Le juge décide si un draft absent est légal. Il l'est, donc l'entrée passe. Mais remplir le trou revient au stockage, au moment de la persistance — une valeur qui apparaîtrait de nulle part pendant la validation est une valeur que l'appelant n'a jamais envoyée et qu'on ne peut pas lui opposer. Juger et réaliser restent séparés : la même entrée jugée deux fois ne gagne jamais un champ en chemin.

Si vous remplacez un schéma Zod dont vous utilisiez le .default() au niveau de la route, cette ligne doit se déplacer là où la ligne est écrite.

Ce qui ne traverse pas

Standard Schema est une seule fonction : validate(value) → value | issues. Elle porte l'axe shape, et rien d'autre.

traverse ~standardreste à la maison
shape — les mots-clés JSON Schema
role — primary, relations, uniquela DDL, le type GraphQL
lifecycle — qui écrit, et quandl'ORM qui l'estampille
boundary — direction, conversionsen partie — le décodage entrant s'appliqueinputFields / outputFields

Un consommateur hors de Fougere obtient donc le juge, dans le navigateur ou sur son serveur, avec les règles qu'applique la façade. Il n'obtient ni la table, ni les surfaces, ni le contrat de formulaire — ceux-là lisent Post.getFields(), qui est un appel Fougere.

Pour la forme sérialisable de la même déclaration — celle qui traverse un processus ou un langage plutôt qu'une frontière de bibliothèque — voir la carte d'identité.

Suite : Handlers — opérations, règles de liaison, portée de sortie.

Construit avec Fougere — ce site tourne sur le framework qu'il documente.