Schema Decorators
Schema decorators are the entry point for the UDIOT configuration system. You annotate a plain TypeScript class with @AIConfig and its properties with @AIEnv; the framework reads those annotations at runtime to build a ConfigSchema that drives loading, validation, and provenance tracking.
reflect-metadata must be imported once in your entry point (or tsconfig emitDecoratorMetadata must be enabled) for type inference to work correctly.
Key Concepts
ConfigSchema— the complete description of one configuration namespace, built bybuildConfigSchema(MyClass).ConfigFieldSchema— per-property metadata stored for each@AIEnv-decorated field.ConfigPolicy— numeric or regex constraints evaluated during the load phase.- Namespace — derived automatically from the class name (e.g.
DatabaseConfig→database). Override with@AIConfig({ namespace: 'db' }). - Environment variable name — derived as
NAMESPACE_FIELDKEYin SCREAMING_SNAKE_CASE. Override with@AIEnv({ env: 'MY_CUSTOM_VAR' }).
@AIConfig(options) — Class Decorator
Marks a class as a configuration schema and registers metadata.
import { AIConfig } from '@tagna/udiot';
@AIConfig({
name?: string; // Human-readable name (defaults to class name)
namespace?: string; // Key used to scope values (auto-derived from class name)
profile?: string[]; // Active profiles; default ['local', 'staging', 'prod']
validate?: boolean; // Run validation on load; default true
secrets?: 'auto' | 'manual'; // 'auto' fetches all sensitive fields; default 'auto'
optimize?: boolean; // Hint to enable optimizations
description?: string; // Documentation string
version?: string; // Schema version for drift tracking
})
class MyConfig {}
When namespace is omitted, the class name is converted to kebab-case and the Config suffix is stripped: DatabaseConfig → database, CacheServiceConfig → cache-service.
@AIEnv(options) — Property Decorator
Describes a single configuration field. All options are optional.
import { AIEnv } from '@tagna/udiot';
class MyConfig {
@AIEnv({
key?: string; // Override the field key (defaults to property name)
path?: string; // Dot-notation path inside the namespace object
env?: string; // Override the environment variable name
type?: 'string' | 'number' | 'boolean' | 'object' | 'array';
// Inferred from TypeScript design type when omitted
required?: boolean; // Missing value → added to missingRequired list
default?: unknown; // Fallback value when no layer provides one
description?: string; // Documentation string
sensitive?: boolean; // Redact in logs and provenance output
provider?: string; // Named secret provider to use
dependsOn?: string[]; // Keys that must be present for this field to be valid
conflictsWith?: string[]; // Keys that must NOT be present alongside this field
validate?: string | string[]; // Named validation rules
optimize?: boolean; // Field-level optimization hint
observe?: boolean; // Emit change events when this value is overridden
enum?: Array<string | number>; // Allowed values; violation → policy error
policy?: ConfigPolicy; // Numeric/regex constraints (see below)
owner?: string; // Team or person responsible for this field
tags?: string[]; // Arbitrary labels for grouping
})
myField!: string;
}
Type inference
When type is not provided, the decorator reads the TypeScript design-time type via reflect-metadata. String → 'string', Number → 'number', Boolean → 'boolean', Array → 'array', Object → 'object'.
Path notation
Setting path: 'server.port' nests the value under values[namespace].server.port. Alternatively, use double underscores in the property name (server__port) and the path is inferred as server.port automatically.
ConfigFieldSchema Interface
The resolved metadata stored per decorated property. This is what buildConfigSchema produces.
interface ConfigFieldSchema {
propertyKey: string; // TypeScript property name on the class
key: string; // Field key (may differ from propertyKey)
path: string; // Dot-notation path inside the namespace object
type: ConfigValueType; // 'string' | 'number' | 'boolean' | 'object' | 'array'
required?: boolean;
default?: unknown;
description?: string;
sensitive?: boolean;
env?: string; // Computed or explicit environment variable name
provider?: string;
dependsOn?: string[];
conflictsWith?: string[];
validate?: string | string[];
optimize?: boolean;
observe?: boolean;
enum?: Array<string | number>;
policy?: ConfigPolicy;
owner?: string;
tags?: string[];
suggestion?: string; // Set by @AISuggested
}
ConfigPolicy Interface
Numeric or pattern constraints evaluated when a value is loaded.
interface ConfigPolicy {
min?: number; // Value must be >= min (applies to number type)
max?: number; // Value must be <= max (applies to number type)
pattern?: string; // Value must match this regular expression (applies to string type)
}
A violation produces a ConfigPolicyViolation in the ConfigLoadResult returned by EnvironmentConfigManager.load().
@AISuggested(reason?) — Property Decorator
Attaches an AI-hint string to a field. The suggestion is surfaced in scaffolded .env.example files and tooling output to guide developers on what value to provide.
import { AIEnv, AISuggested } from '@tagna/udiot';
class MyConfig {
@AISuggested('Set to the connection string from your cloud provider dashboard')
@AIEnv({ required: true, sensitive: true })
databaseUrl!: string;
}
The reason parameter is optional. When omitted the property is still flagged as AI-suggested; the suggestion string is undefined.
API Reference
| Function | Signature | Description |
|---|---|---|
AIConfig |
(options?: AIConfigOptions) => ClassDecorator |
Registers class as a config schema |
AIEnv |
(options?: AIEnvOptions) => PropertyDecorator |
Describes a config field |
AISuggested |
(reason?: string) => PropertyDecorator |
Attaches an AI hint to a field |
buildConfigSchema |
<T>(target: ConfigClass<T>) => ConfigSchema |
Builds the resolved schema for a class |
extractSchemasFromModule |
(moduleExports: unknown) => ConfigSchema[] |
Finds all @AIConfig classes in a module export |
hasConfigDecorator |
(target: Function) => boolean |
Returns true if the class has @AIConfig |
Basic Usage Example
import 'reflect-metadata';
import { AIConfig, AIEnv, AISuggested, buildConfigSchema } from '@tagna/udiot';
@AIConfig({
namespace: 'database',
description: 'PostgreSQL connection settings',
version: '1.0.0',
})
class DatabaseConfig {
@AIEnv({ required: true, description: 'Database host name' })
host!: string;
@AIEnv({ type: 'number', default: 5432, policy: { min: 1, max: 65535 } })
port!: number;
@AISuggested('Retrieve from your secrets manager or Vault')
@AIEnv({ sensitive: true, provider: 'vault', required: true })
password!: string;
@AIEnv({
type: 'number',
default: 10,
policy: { min: 1, max: 100 },
dependsOn: ['host'],
description: 'Maximum number of connections in the pool',
})
maxConnections!: number;
}
const schema = buildConfigSchema(DatabaseConfig);
console.log(schema.namespace); // 'database'
console.log(schema.fields.port.env); // 'DATABASE_PORT'
Common Patterns
Enum validation
@AIEnv({ enum: ['debug', 'info', 'warn', 'error'], default: 'info' })
logLevel!: string;
Nested paths via double-underscore
@AIEnv({ type: 'number', default: 30 })
server__timeoutSeconds!: number;
// resolves to values['myapp'].server.timeoutSeconds
Profile-scoped schemas
@AIConfig({ profile: ['prod'], secrets: 'auto' })
class ProductionOnlyConfig { /* … */ }
Fields on this schema are only validated when the active environment profile matches 'prod'.
Integration Notes
buildConfigSchemainstantiates the class withnew target()to read property initialisers as default values. Ensure the constructor requires no mandatory arguments.- When using module bundlers or tree-shakers, import decorated classes explicitly to prevent decorator side-effects from being stripped.
extractSchemasFromModuleaccepts the result of a dynamicimport()call and returns all@AIConfigclasses found in the export object, making it easy to load schemas from arbitrary files.
Pitfalls
- Forgetting
import 'reflect-metadata'causes type inference to return'string'for every field regardless of the TypeScript type annotation. @AISuggestedmust be applied before@AIEnvin source order (decorators are applied bottom-up in TypeScript).- A field with
sensitive: truethat also has adefaultvalue will have its default exposed in the schema object. Do not set defaults for genuinely secret fields.