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 ~standard | reste à la maison | |
|---|---|---|
shape — les mots-clés JSON Schema | ✅ | |
role — primary, relations, unique | la DDL, le type GraphQL | |
lifecycle — qui écrit, et quand | l'ORM qui l'estampille | |
boundary — direction, conversions | en partie — le décodage entrant s'applique | inputFields / 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.