Skip to main content
Permission Mongo provides comprehensive schema validation to ensure data integrity and consistency across your collections.

Overview

The schema validation system:
  • Type checking - Enforce field types (string, number, date, objectId, etc.)
  • Constraints - Define min/max lengths, patterns, enums, and ranges
  • Required fields - Ensure critical fields are always present
  • Immutable fields - Prevent modification of fields after creation
  • Nested validation - Validate objects and arrays recursively
  • Custom formats - Email, URL, and other format validation

Schema definition

Define schemas in schema.yml:
schema.yml
collections:
  users:
    fields:
      _id:
        type: objectId
        auto: true
      
      email:
        type: string
        required: true
        format: email
      
      name:
        type: string
        required: true
        min_length: 1
        max_length: 255
      
      role:
        type: string
        required: true
        enum: ["admin", "manager", "user", "viewer"]
      
      tenant_id:
        type: objectId
        required: true
      
      created_at:
        type: date
        auto: true
        immutable: true
      
      updated_at:
        type: date
        auto_update: true

Field types

Permission Mongo supports these field types:
string
type
Text values with optional length and pattern constraints
number
type
Floating-point numbers with optional min/max constraints
integer
type
Whole numbers only, with optional min/max constraints
boolean
type
true or false values
date
type
ISO8601 date/time values or native time.Time objects
objectId
type
MongoDB ObjectID (24 hex characters)
uuid
type
UUID string in standard format
array
type
Array of items with optional item schema
object
type
Nested object with property definitions
any
type
Any type - no validation applied
const (
    TypeString   FieldType = "string"
    TypeNumber   FieldType = "number"
    TypeInteger  FieldType = "integer"
    TypeBoolean  FieldType = "boolean"
    TypeDate     FieldType = "date"
    TypeObjectId FieldType = "objectId"
    TypeUUID     FieldType = "uuid"
    TypeArray    FieldType = "array"
    TypeObject   FieldType = "object"
    TypeAny      FieldType = "any"
)

String constraints

Validate string fields with these constraints:
schema.yml
fields:
  username:
    type: string
    required: true
    min_length: 3
    max_length: 50
    pattern: "^[a-zA-Z0-9_]+$"
  
  email:
    type: string
    required: true
    format: email
  
  website:
    type: string
    format: url
  
  status:
    type: string
    enum: ["draft", "published", "archived"]
    default: "draft"
min_length
integer
Minimum string length (inclusive)
max_length
integer
Maximum string length (inclusive)
pattern
string
Regular expression pattern the value must match
format
string
Predefined format: email or url
enum
array
List of allowed values
default
any
Default value if field is not provided

Number constraints

Validate numeric fields:
schema.yml
fields:
  age:
    type: integer
    min: 0
    max: 150
  
  price:
    type: number
    min: 0.01
    max: 999999.99
  
  rating:
    type: number
    min: 0
    max: 5
min
number
Minimum value (inclusive)
max
number
Maximum value (inclusive)

Array validation

Validate arrays and their items:
schema.yml
fields:
  tags:
    type: array
    items:
      type: string
      min_length: 1
      max_length: 50
  
  collaborators:
    type: array
    items:
      type: objectId
  
  metadata:
    type: array
    items:
      type: object
      properties:
        key:
          type: string
          required: true
        value:
          type: string
items
object
Schema for array items. All items must match this schema.

Object validation

Validate nested objects:
schema.yml
fields:
  address:
    type: object
    properties:
      street:
        type: string
        required: true
      city:
        type: string
        required: true
      state:
        type: string
        pattern: "^[A-Z]{2}$"
      postal_code:
        type: string
        pattern: "^[0-9]{5}$"
      country:
        type: string
        default: "US"
properties
object
Map of property names to field schemas. Each property is validated according to its schema.

Field modifiers

Control field behavior with modifiers:
schema.yml
fields:
  _id:
    type: objectId
    auto: true          # Auto-generated on create
  
  email:
    type: string
    required: true      # Must be present
    immutable: true     # Cannot be changed after creation
  
  created_at:
    type: date
    auto: true          # Auto-set on create
    immutable: true     # Cannot be changed
  
  updated_at:
    type: date
    auto_update: true   # Auto-updated on every change
  
  computed_field:
    type: string
    computed: true      # Computed by application logic
required
boolean
default:"false"
Field must be present in documents
immutable
boolean
default:"false"
Field cannot be modified after creation
auto
boolean
default:"false"
Field is automatically generated on document creation
auto_update
boolean
default:"false"
Field is automatically updated on every modification
computed
boolean
default:"false"
Field is computed by hooks or application logic - skipped during validation

Validation process

The validator checks documents in this order:
  1. Required field check - Ensure all required fields are present
  2. Type validation - Verify each field matches its declared type
  3. Constraint validation - Check min/max, patterns, enums, etc.
  4. Nested validation - Recursively validate objects and arrays
// ValidateDocument validates a document for a collection
func (v *Validator) ValidateDocument(collection string, doc map[string]interface{}) []ValidationError {
    var errors []ValidationError

    collConfig, ok := v.schema.GetCollection(collection)
    if !ok {
        errors = append(errors, ValidationError{
            Field:   "",
            Code:    ErrUnknownCollection,
            Message: fmt.Sprintf("unknown collection: %s", collection),
        })
        return errors
    }

    // Validate required fields
    for name, field := range collConfig.Fields {
        if field.Required {
            val, exists := doc[name]
            if !exists || val == nil {
                errors = append(errors, ValidationError{
                    Field:   name,
                    Code:    ErrRequired,
                    Message: fmt.Sprintf("field '%s' is required", name),
                })
            }
        }
    }

    // Validate each field in the document
    for name, value := range doc {
        if name == "_id" {
            continue
        }

        field, exists := collConfig.Fields[name]
        if !exists {
            continue
        }

        if field.Computed != nil {
            continue
        }

        if value == nil && !field.Required {
            continue
        }

        fieldErrors := v.ValidateField(&field, name, value)
        errors = append(errors, fieldErrors...)
    }

    return errors
}

Validation errors

Validation errors include specific error codes:
TYPE_MISMATCH
error
Field type doesn’t match schema
REQUIRED
error
Required field is missing
MIN_LENGTH
error
String is too short
MAX_LENGTH
error
String is too long
PATTERN
error
String doesn’t match pattern
FORMAT
error
String doesn’t match format (email/url)
MIN
error
Number is below minimum
MAX
error
Number exceeds maximum
ENUM
error
Value not in allowed enum list
IMMUTABLE
error
Attempt to modify immutable field

Update validation

When updating documents, the validator checks:
  1. Document validation - Validate the new document
  2. Immutable fields - Ensure immutable fields haven’t changed
// ValidateUpdate validates an update against the old document
func (v *Validator) ValidateUpdate(collection string, oldDoc, newDoc map[string]interface{}) []ValidationError {
    var errors []ValidationError

    collConfig, ok := v.schema.GetCollection(collection)
    if !ok {
        return errors
    }

    // First validate the new document
    docErrors := v.ValidateDocument(collection, newDoc)
    errors = append(errors, docErrors...)

    // Check immutable fields
    for name, field := range collConfig.Fields {
        if !field.Immutable {
            continue
        }

        oldVal, oldExists := oldDoc[name]
        newVal, newExists := newDoc[name]

        if oldExists && (!newExists || !reflect.DeepEqual(oldVal, newVal)) {
            errors = append(errors, ValidationError{
                Field:   name,
                Code:    ErrImmutable,
                Message: fmt.Sprintf("field '%s' is immutable", name),
                Value:   newVal,
            })
        }
    }

    return errors
}

Partial validation

For partial updates (PATCH operations), use partial validation:
// ValidatePartialDocument validates only the fields present
func (v *Validator) ValidatePartialDocument(collection string, doc map[string]interface{}) []ValidationError {
    var errors []ValidationError

    collConfig, ok := v.schema.GetCollection(collection)
    if !ok {
        return errors
    }

    // Validate each field in the document (but don't check required)
    for name, value := range doc {
        if name == "_id" {
            continue
        }

        field, exists := collConfig.Fields[name]
        if !exists || field.Computed != nil || value == nil {
            continue
        }

        fieldErrors := v.ValidateField(&field, name, value)
        errors = append(errors, fieldErrors...)
    }

    return errors
}
This validates only the fields being updated without requiring all fields to be present.

Example schema

Complete example for a documents collection:
schema.yml
collections:
  documents:
    fields:
      _id:
        type: objectId
        required: true
      
      title:
        type: string
        required: true
        max_length: 500
      
      content:
        type: string
      
      status:
        type: string
        enum: ["draft", "published", "archived"]
        default: "draft"
      
      tenant_id:
        type: objectId
        required: true
      
      owner_id:
        type: objectId
        required: true
      
      tags:
        type: array
        items:
          type: string
      
      metadata:
        type: object
        properties:
          category:
            type: string
          priority:
            type: integer
            min: 1
            max: 5
      
      created_at:
        type: date
        auto: true
        immutable: true
      
      updated_at:
        type: date
        auto_update: true
    
    indexes:
      - fields: ["tenant_id", "status"]
      - fields: ["owner_id"]
      - fields: ["tags"]

Best practices

Mark essential fields like tenant_id, owner_id, and identifiers as required: true.
Set created_at, created_by, and other audit fields as immutable: true to preserve history.
Define enum constraints for status fields, types, and other fields with fixed options.
Use format: email and format: url for user-provided data to ensure validity.
Apply max_length to prevent abuse and ensure database performance.
  • RBAC - Control access with role-based policies
  • Hooks - Execute validation logic in pre-operation hooks
  • Versioning - Track document changes over time

Build docs developers (and LLMs) love