Skip to main content
Version: 🚧 Canary

🎯 Overriding Existing Schemas

This guide demonstrates how to override and extend existing Nodeblocks schemas to customize validation rules, add new fields, or modify existing field constraints. We'll show how to extend the built-in profileSchema with custom validation rules and additional required fields. Because the SDK validates through AJV against JSON Schema draft-07, you can use the full draft-07 keyword set directly in withSchema.


🏗️ Schema Override Architecture

Schema overriding allows you to:

  1. Extend existing schemas - Build upon pre-defined schemas from the SDK
  2. Add custom validation - Implement domain-specific validation rules
  3. Modify field constraints - Change min/max values, patterns, or types
  4. Control required fields - Make fields required or optional as needed

1️⃣ Understanding Built-in Schemas

Nodeblocks provides pre-built schemas for common entities. The profileSchema includes standard profile fields:

// Built-in profileSchema structure
{
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
additionalProperties: false,
properties: {
avatar: {
oneOf: [avatarSchema, { type: 'null' }],
},
identityId: { type: 'string' },
name: { type: 'string' },
},
}

createProfileSchema builds on this and requires identityId and name.

Draft-07 Support

The SDK compiles your schemas with AJV in draft-07 mode, so draft-07 keywords are available without any extra adapter layer.

Common draft-07 keywords you can use include:

  • allOf, anyOf, oneOf, not
  • if, then, else
  • const, enum
  • pattern, patternProperties
  • dependencies, contains
  • definitions, format, default

The SDK also applies its own object defaults while walking supported schema branches:

  • additionalProperties: false is applied to object schemas unless you explicitly override it
  • queryFilter: true is applied to object schemas to protect MongoDB-style query filters

That defaulting pass walks properties, allOf, oneOf, anyOf, and array items. For legacy withSchema(schemaA, schemaB) usage, the SDK deep-merges same-type schemas before compiling them. For OpenAPI-style withSchema({ requestBody, parameters }), the SDK enhances the supplied schemas with these defaults before compiling them.

import {primitives} from '@nodeblocks/backend-sdk';

const {withSchema} = primitives;

export const accountSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: {
additionalProperties: false,
if: {
properties: {
accountType: {const: 'organization'},
},
required: ['accountType'],
},
then: {
required: ['organizationName'],
},
else: {
not: {
required: ['organizationName'],
},
},
properties: {
accountType: {enum: ['personal', 'organization']},
organizationName: {type: 'string', minLength: 2},
},
required: ['accountType'],
type: 'object',
},
},
},
required: true,
},
});

2️⃣ Basic Schema Override

Here's how to override an existing schema with custom validation:

src/schemas/customProfileSchema.ts
import {primitives, schemas} from '@nodeblocks/backend-sdk';

const {withSchema} = primitives;
const {profileSchema} = schemas;

export const createCustomProfileSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: {
// Spread the existing schema
...profileSchema,
properties: {
// Spread existing properties
...profileSchema.properties,
// Override or add new properties
age: {
type: 'number',
minimum: 18,
maximum: 100,
},
},
// Preserve existing required fields and add age
required: [...(profileSchema.required || ['identityId', 'name']), 'age'],
},
},
},
required: true,
},
});

You can then compose the custom schema with an existing route:

import {primitives, routes} from '@nodeblocks/backend-sdk';
import {createCustomProfileSchema} from './schemas/customProfileSchema';

const {compose} = primitives;
const {createProfileRoute} = routes;

export const createCustomProfileFeature = compose(createCustomProfileSchema, createProfileRoute);

// Use createCustomProfileFeature instead of features.createProfileFeature

3️⃣ Schema Override Patterns

Pattern 1: Adding Fields

// Add new optional fields
schema: {
...existingSchema,
properties: {
...existingSchema.properties,
newField: { type: 'string' }
}
}

Pattern 2: Making Fields Required

// Make existing optional fields required (always merge, never replace)
schema: {
...existingSchema,
required: [...(existingSchema.required || []), 'existingField']
}

Pattern 3: Overriding Field Constraints

// Override existing field with stricter validation
properties: {
...existingSchema.properties,
name: {
...existingSchema.properties.name,
minLength: 2,
maxLength: 80
}
}

Pattern 4: Removing Fields

// Remove fields by destructuring, then drop them from required too
const { unwantedField, ...allowedProperties } = existingSchema.properties;
const required = (existingSchema.required || []).filter(
(field) => field !== 'unwantedField'
);

schema: {
...existingSchema,
properties: allowedProperties,
required,
}

➡️ Next Steps

Now you can enhance your schemas by:

  • Adding custom validation rules - Implement business-specific constraints
  • Creating reusable schema components - Build a library of common schema patterns
  • Implementing complex validation - Use conditional schemas and custom validators
  • Testing schema validation - Create comprehensive test suites for your overrides

🔗 See Also