Skip to main content

An easy-to-user dictionary validation tool, good for use-cases like POST API data validation.

Project description

required-dict

An easy-to-use Python library for validating dictionary data with chainable validation rules. Build for API data validation, but good for any form processing or configuration validation.

Installation

pip install required_dict

Quick Start

from required_dict import REQUIRED

validation = {
    'user_name': REQUIRED(),
    'user_email': REQUIRED().as_email().none_ok(),
    'user_age': REQUIRED().greater_than(18),
    'user_type': REQUIRED().constrain_to('buyer', 'seller').default('buyer')
}

user_data = {
    'user_name': 'John Doe',
    'user_email': 'john@example.com',
    'user_age': 25
}

validated = REQUIRED.validate_data(validation, user_data)
print(validated['user_type'])  # 'buyer' (from default)

Core Features

Basic Requirements

REQUIRED()                    # Field must exist and not be None
REQUIRED().none_ok()          # Allow None values
REQUIRED().default('value')   # Set default if missing

Type Validation

REQUIRED().as_type(str)                      # Exact type matching
REQUIRED().as_type(int, isinstance=True)     # isinstance() checking

Format Validation

REQUIRED().as_email()         # Email format
REQUIRED().as_uuid()          # Any UUID version
REQUIRED().as_uuid(4)         # Specific UUID version
REQUIRED().as_isotime()       # ISO-8601 timestamp
REQUIRED().as_json()          # Valid JSON

Numeric Validation

REQUIRED().as_posnum()                       # Positive numbers
REQUIRED().as_negnum()                       # Negative numbers
REQUIRED().greater_than(18)                  # > 18
REQUIRED().greater_than(18, or_equal_to=True) # >= 18
REQUIRED().less_than(65)                     # < 65
REQUIRED().less_than(65, or_equal_to=True)   # <= 65

# Chain for ranges
REQUIRED().greater_than(18, True).less_than(65, True)  # 18 <= value <= 65

Constraints & Whitelists

REQUIRED().constrain_to('buyer', 'seller')   # Must be one of these
REQUIRED().whitelist('SYSTEM', 'ADMIN')      # These values bypass validation

Field Aliases

REQUIRED().alias('id', 'user_id', 'party_id')  # Accept multiple field names

Examples

User Registration

validation = {
    'user_name': REQUIRED(),
    'user_email': REQUIRED().as_type(str).as_email(),
    'user_age': REQUIRED().greater_than(13),
    'user_balance': REQUIRED().as_posnum().default(0),
    'user_type': REQUIRED().constrain_to('buyer', 'seller'),
    'user_detail': REQUIRED().as_json(),
    'joined_time': REQUIRED().as_isotime(),
    'notes': 'Default notes'  # Non-REQUIRED field
}

user_data = {
    'user_name': 'John Doe',
    'user_email': 'john@example.com',
    'user_age': 25,
    'user_type': 'buyer',
    'user_detail': '{"preferences": {"theme": "dark"}}',
    'joined_time': '2025-01-01T12:00:00+00:00'
}

validated = REQUIRED.validate_data(validation, user_data)

Handling Missing Fields

# This fails - missing required field
try:
    REQUIRED.validate_data(
        validation={'name': REQUIRED()},
        user_data={}
    )
except ValueError as e:
    print(e)  # name = REQUIRED - Missing

# This fails - None not allowed by default
try:
    REQUIRED.validate_data(
        validation={'name': REQUIRED()},
        user_data={'name': None}
    )
except ValueError as e:
    print(e)  # name = REQUIRED - Present, but Empty

# This works - None explicitly allowed
validated = REQUIRED.validate_data(
    validation={'name': REQUIRED().none_ok()},
    user_data={'name': None}
)

Alias Handling

# Basic usage - all aliases populated
validated = REQUIRED.validate_data(
    validation={'id': REQUIRED().alias('user_id', 'party_id')},
    user_data={'user_id': 'abc123'}
)
# Result: id, user_id, party_id all equal 'abc123'

# Conflicting aliases generate warnings
import warnings
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    validated = REQUIRED.validate_data(
        validation={'id': REQUIRED().alias('user_id', 'party_id')},
        user_data={'user_id': 'abc', 'party_id': 'xyz'}  # Different values!
    )
    if w:
        print(w[0].message)  # Warning about conflicting values

# Convert warnings to errors
try:
    REQUIRED.validate_data(
        validation={'id': REQUIRED().alias('user_id', 'party_id')},
        user_data={'user_id': 'abc', 'party_id': 'xyz'},
        error_on_conflicting_aliases=True
    )
except ValueError as e:
    print(e)  # Error about conflicting aliases

Numeric Ranges

validation = {
    'child_age': REQUIRED().greater_than(0).less_than(13),
    'teen_age': REQUIRED().greater_than(12).less_than(20),  # 13-19
    'adult_age': REQUIRED().greater_than(17),               # 18+
}

# Valid data
REQUIRED.validate_data(validation, {'child_age': 8})   # Valid
REQUIRED.validate_data(validation, {'teen_age': 16})   # Valid
REQUIRED.validate_data(validation, {'adult_age': 25})  # Valid

Email Validation

validation = {
    'email1': REQUIRED().as_email(),
    'email2': REQUIRED().as_email(),
    'email3': REQUIRED().as_email(),
}

# Valid emails
valid_emails = {
    'email1': 'user@example.com',
    'email2': 'test.email@company.co.uk',
    'email3': 'user_name@sub-domain.com',
}

validated = REQUIRED.validate_data(validation, valid_emails)

# Invalid emails will raise ValueError
invalid_emails = {
    'email1': 'user@domain',      # No TLD
    'email2': 'user.domain.com',  # No @
    'email3': '@domain.com',      # No local part
}

UUID Validation

validation = {
    'any_uuid': REQUIRED().as_uuid(),           # Any version
    'uuid_v4': REQUIRED().as_uuid(4),          # UUIDv4 only
    'uuid_v7': REQUIRED().as_uuid(7),          # UUIDv7 only
    'special_id': REQUIRED().as_uuid().whitelist('SYSTEM')  # UUID or special value
}

valid_data = {
    'any_uuid': '550e8400-e29b-41d4-a716-446655440000',
    'uuid_v4': '550e8400-e29b-41d4-a716-446655440000',
    'uuid_v7': '01234567-1234-7654-8765-123456789012',
    'special_id': 'SYSTEM'  # Bypasses UUID validation
}

Complex Validation

# API payload validation
validation = {
    'request_id': REQUIRED().as_uuid(4),
    'timestamp': REQUIRED().as_isotime(),
    'payload': REQUIRED().as_json(),
    'user_id': REQUIRED().alias('id', 'userid'),
    'action': REQUIRED().constrain_to('create', 'update', 'delete'),
    'priority': REQUIRED().greater_than(0).less_than(11).default(5),
    'metadata': REQUIRED().none_ok().default(None)
}

api_data = {
    'request_id': '550e8400-e29b-41d4-a716-446655440000',
    'timestamp': '2025-01-01T12:00:00+00:00',
    'payload': '{"action": "user_update"}',
    'id': 'user_12345',  # Populates all aliases
    'action': 'update'
}

validated = REQUIRED.validate_data(validation, api_data)

Configuration Options

Key Normalization

# Default: keys lowercased automatically
data = {'USER_NAME': 'John', 'Id': '123'}
validation = {'user_name': REQUIRED(), 'id': REQUIRED()}
validated = REQUIRED.validate_data(validation, data)  # Works

# Disable key lowercasing
validated = REQUIRED.validate_data(
    validation={'USER_NAME': REQUIRED()},
    user_data={'USER_NAME': 'John'},
    keys_lowercased=False
)

Value Trimming

# Default: string values trimmed
user_data = {'name': '  John  '}
validated = REQUIRED.validate_data({'name': REQUIRED()}, user_data)
print(validated['name'])  # 'John'

# Disable trimming
validated = REQUIRED.validate_data(
    validation={'name': REQUIRED()},
    user_data={'name': '  John  '},
    values_trimmed=False
)
print(validated['name'])  # '  John  '

Error Handling

# Disable errors for missing fields - this effectively removes validation
validated = REQUIRED.validate_data(
    validation={'required_field': REQUIRED()},
    user_data={},
    error_on_missing_required=False
)

# Control alias conflicts
REQUIRED.validate_data(
    validation={'id': REQUIRED().alias('user_id')},
    user_data={'user_id': 'abc', 'id': 'xyz'},
    error_on_conflicting_aliases=True  # Raises error instead of warning
)

Error Messages

try:
    REQUIRED.validate_data(
        validation={
            'user_name': REQUIRED(),
            'user_email': REQUIRED().as_email(),
            'user_age': REQUIRED().greater_than(18)
        },
        user_data={
            'user_email': 'susy@example,com',
            'user_age': 16
        }
    )
except ValueError as e:
    print(e)
    # Output:
    # One or more required fields were missing, left empty, or of the wrong type:
    #   user_name = REQUIRED - Missing
    #   user_email = REQUIRED - Present, but Not Valid Email Format (susy@example,com)
    #   user_age = REQUIRED - Present, but Not Greater Than 18 (16)
    #
    # Full signature (JSON keys):
    #   Required: user_name, user_email, user_age
    #   Optional: 

Advanced Features

Method Chaining

# All methods return self for chaining
REQUIRED().as_type(str).as_email().none_ok().default('admin@system.com')
REQUIRED().as_type(int).greater_than(0).less_than(100).default(50)

Debugging Rules

rule = REQUIRED().as_email().as_type(str).none_ok()
print(rule)
# Output:
# REQUIRED: KEY MUST EXIST
# REQUIRED: type : [<class 'str'>] (isinstance: False)
# REQUIRED: noneok : True
# REQUIRED: email : True

Class vs Instance Usage

# Both work
REQUIRED.validate_data(validation, data)      # Class method
REQUIRED().validate_data(validation, data)    # Instance method

# Rules must be instances
validation = {
    'field': REQUIRED()    # ✓ Correct
    # 'field': REQUIRED   # ✗ Wrong
}

Example Use-Cases

API Validation

def validate_user_update(request_data):
    validation = {
        'user_id': REQUIRED().as_uuid().alias('id'),
        'name': REQUIRED().as_type(str),
        'email': REQUIRED().as_email(),
        'age': REQUIRED().greater_than(12).less_than(120),
        'role': REQUIRED().constrain_to('user', 'admin').default('user')
    }
    return REQUIRED.validate_data(validation, request_data)

Configuration Validation

def validate_config(config_dict):
    validation = {
        'database_url': REQUIRED().as_type(str),
        'port': REQUIRED().greater_than(1024).less_than(65536).default(8080),
        'debug': REQUIRED().as_type(bool).default(False),
        'log_level': REQUIRED().constrain_to('DEBUG', 'INFO', 'WARNING', 'ERROR').default('INFO')
    }
    return REQUIRED.validate_data(validation, config_dict)

License

MIT License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

required_dict-0.21.tar.gz (10.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

required_dict-0.21-py3-none-any.whl (5.0 kB view details)

Uploaded Python 3

File details

Details for the file required_dict-0.21.tar.gz.

File metadata

  • Download URL: required_dict-0.21.tar.gz
  • Upload date:
  • Size: 10.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.5

File hashes

Hashes for required_dict-0.21.tar.gz
Algorithm Hash digest
SHA256 a852e319b79050e36492fc9d3982ae9a2342ee0cd60f0affc7772ed9eaa72b21
MD5 fa5a2b5c81e0dd31c4645c0bc3e62b2a
BLAKE2b-256 6fc5cd4ca7b3f3444358ed45b6bc3852ebaa99fbb8b4ce0006e132c11cc4f600

See more details on using hashes here.

File details

Details for the file required_dict-0.21-py3-none-any.whl.

File metadata

  • Download URL: required_dict-0.21-py3-none-any.whl
  • Upload date:
  • Size: 5.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.5

File hashes

Hashes for required_dict-0.21-py3-none-any.whl
Algorithm Hash digest
SHA256 f95bef4b1d8e7d51b32074cb972891931f10c70a7f7f429049165e45e9df1f12
MD5 d918975de8605793f7ccd46acbe1b198
BLAKE2b-256 a37f24460d6068575d8cba5af8e13e2ea9e3cf51d13695d7cf4fad55eb5e9940

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page