Skip to content

Unknown Property Policy ​

QuickModel 1.0 provides three strategies for handling unknown properties in input data through the unknownPropertyPolicy option.

Default Behavior

By default, QuickModel uses 'strip' policy, silently removing unknown properties from input data.

What is Unknown Property Policy? ​

When deserializing data, QuickModel can encounter properties that are not explicitly defined in your model. The unknownPropertyPolicy option controls how these properties are handled.

Three available strategies:

  • 'keep': Preserves extra properties on the instance
  • 'strip' (default): Silently removes extra properties (useful for sanitization)
  • 'error': Throws an error when unknown properties are detected (strictest)

This is useful for:

  • Preventing data pollution (policy: 'error')
  • Sanitizing untrusted input (policy: 'strip')
  • Detecting typos in backend responses (policy: 'error')
  • Flexible data handling (policy: 'keep')

How to Configure ​

Per Class ​

You can set the policy by passing unknownPropertyPolicy in the second argument of the @Quick decorator.

typescript
import { QModel, Quick } from 'quickmodel';

interface IUser {
	name: string;
}

// ✅ Reject unknown properties (strictest)
@Quick({}, { unknownPropertyPolicy: 'error' })
class StrictUser extends QModel<IUser> {
	declare name: string;
}

// ✅ Strip unknown properties (sanitization)
@Quick({}, { unknownPropertyPolicy: 'strip' })
class SanitizedUser extends QModel<IUser> {
	declare name: string;
}

// ✅ Keep unknown properties (most flexible)
@Quick({}, { unknownPropertyPolicy: 'keep' })
class FlexibleUser extends QModel<IUser> {
	declare name: string;
}

You can enforce a global policy for your entire application using QConfig. This is the recommended approach for security-critical applications.

typescript
import { QConfig } from 'quickmodel';

// Call this at the start of your application (e.g. index.ts or server.ts)
QConfig.configure({
	defaults: {
		unknownPropertyPolicy: 'error', // Reject unknown properties by default
	},
});

When configured globally, you can still override for specific legacy classes:

typescript
// Override global policy locally for legacy/flexible models
@Quick({}, { unknownPropertyPolicy: 'keep' })
class LegacyData extends QModel<any> {
	// ...
}

Usage Examples ​

new vs create

User.create(data) is a convenience alias for new User(data) — both are fully equivalent. You can use whichever form you prefer throughout your application.

Policy: 'error' (Strictest) ​

typescript
@Quick({}, { unknownPropertyPolicy: 'error' })
class User extends QModel<IUser> {
	declare name: string;
}

// Correct usage — both forms work identically
new User({ name: 'Alice' }); // ✅ OK
User.create({ name: 'Alice' }); // ✅ OK

// Incorrect usage - Throws Error
new User({
	name: 'Alice',
	isAdmin: true, // ❌ Error: Strict Mode: Property 'isAdmin' is not defined in model User
});
User.create({
	name: 'Alice',
	isAdmin: true, // ❌ Same error
});

Policy: 'strip' (Sanitization) ​

typescript
@Quick({}, { unknownPropertyPolicy: 'strip' })
class User extends QModel<IUser> {
	declare name: string;
}

// Using new
const user1 = new User({
	name: 'Alice',
	isAdmin: true, // Will be removed silently
});

// Using create (equivalent)
const user2 = User.create({
	name: 'Alice',
	isAdmin: true, // Will be removed silently
});

console.log(user1.name); // 'Alice'
console.log((user1 as any).isAdmin); // undefined (stripped)

Policy: 'keep' (Flexible) ​

typescript
@Quick({}, { unknownPropertyPolicy: 'keep' })
class User extends QModel<IUser> {
	declare name: string;
}

// Using new
const user1 = new User({
	name: 'Alice',
	isAdmin: true, // Will be preserved
});

// Using create (equivalent)
const user2 = User.create({
	name: 'Alice',
	isAdmin: true, // Will be preserved
});

console.log(user1.name); // 'Alice'
console.log((user1 as any).isAdmin); // true (kept)

Property Requirements for 'error' Policy ​

For a property to be "accepted" when using unknownPropertyPolicy: 'error', it must meet at least one of these conditions:

  1. Be in the transformation map:

    typescript
    @Quick({ createdAt: Date }) // 'createdAt' is known
  2. Be decorated with @QType:

    typescript
    @QType() declare name: string; // 'name' is known via metadata
  3. Exist physically at runtime (initialized):

    typescript
    class User {
    	role: string = 'guest'; // 'role' is known because it exists on the instance
    }

Watch out for declare

If you use declare property: type; without @QType or @Quick({...}), that property does not exist at runtime (JavaScript). With unknownPropertyPolicy: 'error', attempting to assign a value will fail.

Solution: Register the property in @Quick (e.g., { name: String }) or use @QType().

Complete Example ​

typescript
interface IProduct {
	id: number;
	tags: string[];
}

@Quick(
	{
		tags: [String], // Explicitly registered
	},
	{
		unknownPropertyPolicy: 'error', // 🛡️ Strict validation
	}
)
class Product extends QModel<IProduct> {
	declare id: number; // ⚠️ WARNING: If not registered, this will fail
	declare tags: string[];
}

// ❌ This will fail because 'id' is 'declare' and not in @Quick
new Product({ id: 1, tags: ['a'] });
Product.create({ id: 1, tags: ['a'] }); // same error

// ✅ Correct Solution:
@Quick(
	{
		id: Number, // Register primitive
		tags: [String],
	},
	{ unknownPropertyPolicy: 'error' }
)
class ProductFixed extends QModel<IProduct> {
	declare id: number;
	declare tags: string[];
}

// Both forms work:
const p1 = new ProductFixed({ id: 1, tags: ['a', 'b'] });
const p2 = ProductFixed.create({ id: 1, tags: ['a', 'b'] });

Security Considerations ​

For security-critical applications (APIs, data processing):

  • ✅ Use 'error' for public-facing APIs to prevent mass-assignment attacks
  • ✅ Use 'strip' for sanitizing untrusted input before processing
  • ⚠️ Use 'keep' only for internal/trusted data sources

See SECURITY.md for comprehensive security guidelines.