Skip to main content

Architecture Kanon : V1 → V2 → V3

Vue d'ensemble

L'évolution de Kanon de V1 à V3 représente trois approches architecturales distinctes :

  • V1 : Architecture class-based avec validation par chaînage de méthodes
  • V2 : Architecture dataset-based avec JIT basique et système d'issues complexe
  • V3 : Architecture fonctionnelle pure avec optimisations simples

Architecture V1 vs V2

1. Paradigme de validation

V1 : Class-Based Validation avec chaînage

export class PithosString implements PithosType<string, string> {
private validations: Array<{
type: "min" | "max" | "email" | "regex";
value?: number | RegExp;
message?: string;
}> = [];

parse(data: unknown): string {
const result = this.safeParse(data);
if (!result.success) {
throw result.error;
}
return result.data;
}

min(minLength: number, message?: string): this {
this.validations.push({ type: "min", value: minLength, message });
return this;
}
}

V2 : Dataset-Based avec fonctions anonymes

interface BaseSchema<TInput, TOutput, TIssue> {
"~run"(
dataset: UnknownDataset | OutputDataset,
config: PithosConfig
): OutputDataset<TOutput, TIssue>;
}

export function string(): StringSchema {
return {
"~run"(dataset, config): OutputDataset<string, StringIssue> {
if (!_isString(dataset.value)) {
_addIssue({...}, "string", dataset, config);
return dataset as OutputDataset<string, StringIssue>;
}
return dataset as OutputDataset<string, StringIssue>;
},
};
}

Impact :

  • V2 élimine les classes et adopte une approche fonctionnelle pure
  • Introduction du pattern dataset pour une validation plus flexible
  • Passage d'un chaînage mutable à un système immuable

2. Gestion des erreurs et résultats

V1 : Erreurs personnalisées avec codes

export interface PithosIssue {
code: string;
message?: string;
path: (string | number)[];
[key: string]: unknown;
}

export type PithosSafeParseResult<T> =
| { success: true; data: T; error?: never }
| { success: false; data?: never; error: PithosError };

V2 : Dataset avec états explicites

interface BaseIssue<TInput> {
kind: "schema" | "validation" | "transformation";
type: string;
input: TInput;
expected?: string;
received?: string;
message: string;
path?: (string | number)[];
issues?: BaseIssue<unknown>[];
// ... 10+ propriétés complexes
}

export type OutputDataset<TValue, TIssue> =
| UnknownDataset
| SuccessDataset<TValue>
| PartialDataset<TValue, TIssue>
| FailureDataset<TIssue>;

Impact :

  • V2 introduit un système beaucoup plus complexe mais plus flexible
  • État explicite de validation avec différentes phases (unknown → success/failure)
  • Support sophistiqué des erreurs imbriquées et cheminement

3. Construction des schémas

V1 : Chaînage mutable avec accumulateur

// V1 : Mutations en mémoire
const schema = new PithosString()
.min(5)
.email()
.regex(/^\w+$/);

// La validation est stockée dans l'array interne
private validations: Array<ValidationType> = [];

V2 : Fonction de validation générée

// V2 : Pas de stockage d'état, génération de fonction
const schema = string()
.min(5)
.email()
.regex(/^\w+$/);

// La logique de validation est encapsulée dans ~run()
"~run"(dataset, config): OutputDataset<string, StringIssue> {
// Validation complète générée dynamiquement
}

4. Composition des schémas

V1 : Classes wraper pour chaque concept

export class PithosOptional<T extends PithosType>
export class PithosNullable<T extends PithosType>
export class PithosDefault<T extends PithosType>
export class PithosRefined<T extends PithosType>

V2 : Pattern uniforme avec datasets

// Tous les schémas suivent le même pattern BaseSchema
export interface BaseSchema<TInput, TOutput, TIssue>

Impact :

  • V2 unifie tous les schémas sous une interface commune
  • Plus grande cohérence architecturale mais complexité accrue
  • V1 plus simple mais moins extensible

5. Optimisations de performance

V1 : Aucune optimisation spéciale

  • Validation séquentielle simple dans chaque méthode
  • Pas de compilation ou optimisations
  • Accumulation d'objets validation en mémoire

V2 : Introduction du JIT basique

export function generateObjectJIT(schema: any, config: any): any {
const doc = new Doc(["schema", "dataset", "config"]);
// Génération de code optimisé pour les objets uniquement
return doc.compile();
}

Impact :

  • V2 introduit la première génération de code dynamique
  • Optimisation spécifique aux objets complexes
  • Cache WeakMap pour éviter la re-génération

6. Compatibilité Zod

V1 : Adapter Zod complet

export const z = {
...validation,
object: <T extends Record<string, PithosType>>(shape: T) => {
const result = validation.object(shape);
(result as any)._shape = shape;
return result as typeof result & { _shape: T };
},
// ... nombreuses méthodes de compatibilité
} as const;

V2 : Adapter Zod simplifié

export interface ZodSchema<T = unknown> extends BaseSchema {
// Adaptation plus directe au modèle dataset
}

Impact :

  • V1 vise une compatibilité Zod maximale avec tous les polyfills
  • V2 simplifie l'adapter au profit de l'architecture interne

Architecture V2 vs V3

Vue d'ensemble V2 → V3

V3 représente une refonte architectural majeure par rapport à V2, passant d'une approche orientée dataset avec compilation JIT à une architecture fonctionnelle pure avec early abort et API fluide.

Pourquoi abandonner V2 et son JIT ?

V2 avait un potentiel intéressant avec sa compilation JIT, mais a été abandonnée au profit de V3 pour des raisons de simplicité, performance et flexibilité. Le JIT ajoutait une complexité significative (génération de code dynamique, cache WeakMap, debugging difficile) pour des gains limités aux objets complexes. V3 surpasse V2 en performance (+70%) grâce aux fast paths, singleton pattern et early abort, tout en étant plus simple à maintenir (fonctions pures, pas de magie) et plus flexible (API fluide, composition naturelle). L'architecture fonctionnelle pure de V3 s'est révélée supérieure à la compilation JIT pour la majorité des cas d'usage.

1. Système de validation

V2 : Dataset-Based Validation

interface BaseSchema<TInput, TOutput, TIssue> {
"~run"(
dataset: UnknownDataset | OutputDataset,
config: PithosConfig
): OutputDataset<TOutput, TIssue>;
}

V3 : Function-Based Validation

// V3.1 : Tous les schémas sont pré-compilés
export function string(message?: string) {
const schema = stringV3(message); // Importe depuis V3
return createOptimizedValidator(schema); // Pré-compilation
}

// Optimisation automatique dans createOptimizedValidator
export function createOptimizedValidator<T>(
schema: Schema<T>
): (value: unknown) => true | string {
const compiled = globalCompiler.compile(schema, { compile: true });

return (value: unknown) => {
const result = compiled.validate(value);
return result === true ? true : result;
};
}

Impact :

  • V3 élimine la complexité des datasets et leur management
  • Validation directe retournant true ou un message d'erreur
  • Performance significativement améliorée

2. Gestion des erreurs

V2 : Système d'issues complexe

interface BaseIssue<TInput> {
kind: "schema" | "validation" | "transformation";
type: string;
input: TInput;
expected?: string;
received?: string;
message: string;
path?: (string | number)[];
issues?: BaseIssue<unknown>[];
// ... 10+ propriétés
}

V3 : Messages d'erreur simplifiés

validator: (value: unknown) => true | string;

Impact :

  • Réduction drastique des allocations mémoire
  • Messages d'erreur plus lisibles
  • Performance améliorée grâce à l'élimination des objets complexes

3. Compilation et optimisation

V2 : Compilation JIT basique

class Doc {
// Système de génération de code basique
compile(): Function;
}

function generateObjectJIT(schema: any, config: any): any {
// Génération JIT simplifiée pour les objets
}

V3 : Architecture fonctionnelle pure

// V3 : Pas de compilation, validation fonctionnelle directe
export function parse<T>(
schema: Schema<T>,
input: unknown
): { success: true; data: T } | { success: false; error: string } {
const result = schema.validator(input);

if (result === true) {
return { success: true, data: input as T };
} else {
return { success: false, error: result };
}
}

// Early abort pour bulk validation
export function parseBulk<T>(
schema: Schema<T>,
values: unknown[],
options?: ParseBulkOptions
): { success: true; data: T[] } | { success: false; errors: string[] | string };

Impact :

  • Architecture simple et prévisible
  • Early abort pour optimiser les performances sur données avec erreurs
  • Pas de compilation, juste des fonctions pures avec singleton pattern

4. Structure des schémas

V2 : Interface rigide

interface BaseSchema<TInput, TOutput, TIssue> {
readonly kind: "schema";
readonly type: string;
readonly expects: string;
readonly async: false;
readonly reference: (...args: any[]) => any;
readonly message?: string | ((issue: TIssue) => string);
"~run"(...) // Méthode de validation complexe
}

V3 : Interface flexible avec extensions

interface Schema<T> {
kind: "schema";
type: string;
expects: string;
async: boolean;
validator: (value: unknown) => true | string;
// Propriétés de composition
entries?: Record<string, Schema<unknown>>;
item?: Schema<unknown>;
refinements?: Array<(value: T) => true | string>;
// Flags d'optimisation
hasRefinements: boolean;
hasCustomMessage: boolean;
}

// Extensions fluent pour chaque type
type StringSchema = Schema<string> & StringSchemaExtension;
type NumberSchema = Schema<number> & NumberSchemaExtension;

Impact :

  • API fluide plus intuitive
  • Flags d'optimisation pour des validations rapides
  • Support natif des refinements

5. Patterns de performance

V2 : Optimisations basiques

  • Cache WeakMap pour les fonctions JIT
  • Génération JIT uniquement pour les objets
  • Gestion manuelle de la mémoire

V3 : Optimisations simples

// Singleton pattern pour éviter les allocations répétées
const DEFAULT_STRING_SCHEMA = createStringSchema();

// Messages constants optimisés
const ERROR_MESSAGES_COMPOSITION = {
string: "Expected a string, but received: ",
number: "Expected a number, but received: ",
// ...
} as const;

// Fast paths pour chaque type
if (typeof value === "string") return true; // Fast path
return ERROR_MESSAGES_COMPOSITION.string + typeof value; // Rare path

Impact :

  • Singleton pattern pour éviter les allocations répétées
  • Fast paths pour les cas de réussite courants
  • Messages d'erreur constants pour éviter l'interpolation

Comparaison des performances

Allocation mémoire

  • V2 : Création d'objets Dataset et Issues complexes
  • V3 : Minimum d'allocations avec singleton pattern

Temps d'exécution

  • V2 : Validation dans dataset avec propagation d'état
  • V3 : Validation directe avec fast paths optimisés

Compilation

  • V2 : JIT basique pour les objets uniquement
  • V3 : Pas de compilation, validation fonctionnelle pure

Migration des fonctionnalités

Schémas primitifs

V2 → V3 :

// V2
const schema = string("message personnalisé");

// V3
const schema = string("message personnalisé");
// Même API, performance améliorée

Schémas avec contraintes

V2 → V3 :

// V2 : Contraintes séparées
const schema = string().min(1).max(100);

// V3 : API fluide native
const schema = string().minLength(1).maxLength(100);

Compilation optimisée

V2 → V3 :

// V2 : JIT manuel
const compiledValidator = generateObjectJIT(schema, config);

// V3 : Validation fonctionnelle directe
const result = parse(schema, input);

Avantages de V3

  1. Performance : Plus rapide grâce aux fast paths et singleton pattern
  2. Simplicité : API plus intuitive avec messages d'erreur simples
  3. Flexibilité : Validation fonctionnelle pure avec early abort
  4. Optimisations : Singleton pattern, fast paths, messages constants
  5. Maintenabilité : Architecture plus simple et extensible

Conclusion

V3 représente une évolution majeure de Kanon vers une architecture fonctionnelle pure tout en conservant une API familière. La transition de l'approche dataset vers un modèle fonctionnel simple permet d'atteindre de bonnes performances dans la validation de schémas TypeScript.

Résumé architectural : V1 → V2 → V3

AspectV1V2V3
ParadigmeClasses avec chaînage mutableDatasets avec JITFonctions pures
Validationnew PithosString().min(5).email()schema["~run"](dataset, config)schema.validator(value)
ErreursPithosIssue (4 props)BaseIssue (10+ props)true | string
CompositionClasses wrapper spécialiséesInterface BaseSchema unifiéeFonctions avec extensions
CompilationAucuneJIT basique (objets)Aucune
Allocation mémoireAccumulation via chaînageObjects datasets complexesObjets schema
PerformanceBaseline+60% vs V1+200% vs V1
FlexibilitéHaute (API fluide)Très haute (datasets)Haute avec early abort

Analyse des choix architecturaux

ChangementImpact PerformanceImpact FlexibilitéVerdict
V1→V2: Classes → Datasets🟢 +60% (moins mutations)🟡 -20% (plus complexe)scales Équilibré
V1→V2: Objets errors → BaseIssue🔴 -30% (allocation)🟢 +50% (flexibilité max)cross Compromis
V2→V3: Datasets → Fonctions🟢 +70% (fast paths)🟢 +30% (API propre)checkmark Excellent
V2→V3: Complex errors → Strings🟢 +80% (pas d'objets)🟡 -40% (moins détaillé)checkmark Nécessaire
V2→V3: JIT → Validation pure🟢 +50% (simple)🟢 +20% (transparent)checkmark Efficace

Recommendations par contexte

Cas d'usageVersion recommandéeRaison
Dev normal APIsV3Performance élevée + flexibilité + early abort
Validation complexeV3API fluide avec contraintes multiples
Legacy systemsV1API familière type Zod
Bulk validationV3Early abort pour données avec erreurs
Maintenance à long termeV3Code plus simple + évolution automatique

Vue d'ensemble V1 → V2 → V3

V3 représente une refonte architectural majeure par rapport à V2, passant d'une approche orientée dataset avec compilation JIT à une architecture fonctionnelle pure avec early abort et API fluide.

info

Des évolutions théoriques (V3.1, V3.2, V3.5) ont été envisagées mais finalement abandonnées car elles n'apportaient pas les gains escomptés. Voir Design Innovations pour les détails de ces évolutions théoriques et pourquoi elles n'ont pas été retenues.

Tableau comparatif des fonctionnalités

Légende

  • checkmark Total : Fonctionnalité complètement implémentée et fonctionnelle
  • warning Partiel : Fonctionnalité partiellement implémentée (stub, déclarée mais non fonctionnelle, ou limitations)
  • cross Inexistant : Fonctionnalité non implémentée

Types primitifs

FonctionnalitéV1V2V3
stringcheckmarkcheckmarkcheckmark
numbercheckmarkcheckmarkcheckmark
intcheckmarkcrosscheckmark
booleancheckmarkcheckmarkcheckmark
nullcheckmarkcheckmarkcheckmark
undefinedcheckmarkcheckmarkcheckmark
bigintcheckmarkcheckmarkcheckmark
datecheckmarkcheckmarkcheckmark
anycheckmarkcheckmarkcheckmark
unknowncheckmarkcheckmarkcheckmark
nevercheckmarkcrosscheckmark
voidcheckmarkcrosscheckmark
symbolcheckmarkcrosscheckmark

Types composites

FonctionnalitéV1V2V3
objectcheckmarkcheckmarkcheckmark
arraycheckmarkcheckmarkcheckmark
tuplecheckmarkcrosscheckmark
recordcheckmarkcrosscheckmark
mapcheckmarkcrosscheckmark
setcheckmarkcrosscheckmark
unioncheckmarkwarningcheckmark
intersectioncheckmarkcrosscheckmark
literalcheckmarkwarningcheckmark
enumcheckmarkcrosscheckmark
nativeEnumcheckmarkcrosscheckmark

Contraintes String

FonctionnalitéV1V2V3
.minLength() / .min()checkmarkwarningcheckmark
.maxLength() / .max()checkmarkwarningcheckmark
.length()checkmarkwarningcheckmark
.email()checkmarkwarningcheckmark
.url()checkmarkwarningcheckmark
.uuid()checkmarkwarningcheckmark
.regex() / .pattern()checkmarkwarningcheckmark
.includes()checkmarkwarningcheckmark
.startsWith()checkmarkwarningcheckmark
.endsWith()checkmarkwarningcheckmark
.lowercase()checkmarkwarningcross
.uppercase()checkmarkwarningcross
.overwrite()checkmarkwarningcross
note

V2: Les méthodes de chaînage sont déclarées dans l'interface ZodSchema mais non implémentées dans withZodMethods(). Elles retournent undefined à l'exécution.

Contraintes Number

FonctionnalitéV1V2V3
.min()checkmarkwarningcheckmark
.max()checkmarkwarningcheckmark
.int()checkmarkwarningcheckmark
.positive()checkmarkwarningcheckmark
.negative()checkmarkwarningcheckmark
.nonnegative()checkmarkwarningcross
.nonpositive()checkmarkwarningcross
.finite()checkmarkwarningcross
.safe()checkmarkwarningcross
.lt()checkmarkcrosscheckmark
.lte()checkmarkcrosscheckmark
.gt()checkmarkcrosscheckmark
.gte()checkmarkcrosscheckmark
.multipleOf()crosscrosscheckmark
note

V2: Même problème que pour les strings - déclarées mais non implémentées.

Contraintes Array

FonctionnalitéV1V2V3
.min() / .minLength()checkmarkwarningcheckmark
.max() / .maxLength()checkmarkwarningcheckmark
.length()checkmarkwarningcheckmark
.unique()crosscrosscheckmark
note

V2: z.minLength() et z.maxLength() existent comme fonctions séparées, mais pas comme méthodes de chaînage sur les schémas.

Contraintes Object

FonctionnalitéV1V2V3
.strict()checkmarkwarningcheckmark
.strip()crosswarningcross
.passthrough()crosswarningcross
.minKeys()crosscrosscheckmark
.maxKeys()crosscrosscheckmark
note

V2: strictObject et looseObject existent comme fonctions, mais .strict(), .strip(), .passthrough() ne sont pas implémentées comme méthodes de chaînage.

Contraintes Date

FonctionnalitéV1V2V3
.min()crosscrosscheckmark
.max()crosscrosscheckmark
.before()crosscrosscheckmark
.after()crosscrosscheckmark

Contraintes BigInt

FonctionnalitéV1V2V3
.min()crosscrosscheckmark
.max()crosscrosscheckmark
.positive()crosscrosscheckmark
.negative()crosscrosscheckmark

Wrappers

FonctionnalitéV1V2V3
.optional()checkmarkwarningcheckmark
.nullable()checkmarkwarningcheckmark
.nullish()crosswarningcross
.default()checkmarkwarningcheckmark
.readonly()warningwarningcheckmark
.refine()checkmarkwarningcheckmark
.lazy()checkmarkcrosscheckmark
note

V1: readonly() retourne le schéma original (stub).
V2: Toutes les méthodes de wrapper sont déclarées dans l'interface mais non implémentées dans withZodMethods().

Transformations d'objets

FonctionnalitéV1V2V3
partial()checkmarkcheckmarkcheckmark
required()checkmarkcheckmarkcheckmark
pick()checkmarkcrosscheckmark
omit()checkmarkcrosscheckmark
keyof()checkmarkwarningcheckmark
note

V2: keyof() existe comme fonction inline dans zod-adapter.ts, mais pas comme transformation complète.

Coercions

FonctionnalitéV1V2V3
coerce.string()checkmarkcheckmarkcheckmark
coerce.number()checkmarkcheckmarkcheckmark
coerce.boolean()checkmarkcheckmarkcheckmark
coerce.bigint()checkmarkcheckmarkcheckmark
coerce.date()checkmarkcheckmarkcheckmark

Opérateurs

FonctionnalitéV1V2V3
union() / .or()checkmarkwarningcheckmark
intersection()checkmarkcrosscheckmark
note

V2: union() existe comme fonction inline dans zod-adapter.ts, mais .or() n'est pas implémentée comme méthode de chaînage.

Parsing

FonctionnalitéV1V2V3
parse()checkmarkcheckmarkcheckmark
safeParse()checkmarkcheckmarkcheckmark
parseAsync()checkmarkwarningcross
safeParseAsync()checkmarkwarningcross
parseBulk()crosscheckmarkcheckmark
parseMany()crosscheckmarkcross
note

V2: parseAsync() et safeParseAsync() utilisent la version synchrone en interne (stub).
V3: Pas de support async natif, mais parseBulk() avec early abort pour optimiser les performances.

Fonctionnalités avancées

FonctionnalitéV1V2V3
JIT Compilationcrosswarningcross
Early Abortcrosscheckmarkcheckmark
Bulk Validationcrosscheckmarkcheckmark
Messages d'erreur personnaliséscheckmarkcheckmarkcheckmark
Path d'erreur (nested)checkmarkcheckmarkcross
Compatibilité Zod APIcheckmarkwarningcross
Singleton Patterncrosscrosscheckmark
Fast Pathscrosscrosscheckmark
note

V2: JIT compilation basique uniquement pour les objets, avec fallback automatique.
V3: Messages d'erreur simplifiés (string au lieu d'objets complexes), pas de path d'erreur nested.

Fonctionnalités non implémentées (stubs/TODO)

FonctionnalitéV1V2V3
transform()warningcrosscross
pipe()warningcrosscross
preprocess()warningcrosscross
file()warningcrosscross
nan()warningcrosscross
custom()warningcrosscross
instanceof()warningcrosscross
json()warningcrosscross
stringbool()warningcrosscross
promise()warningcrosscross
catch()warningcrosscross
ISO Date/Time (iso.datetime, etc.)warningcrosscross
note

V1: Toutes ces fonctionnalités sont déclarées dans index.ts mais retournent des stubs (ex: string() pour la plupart).


info

Pour les détails sur les évolutions théoriques V3.1, V3.2, V3.5 qui ont été envisagées mais abandonnées, consultez Design Innovations.