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.22.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.22-py3-none-any.whl (5.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: required_dict-0.22.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.22.tar.gz
Algorithm Hash digest
SHA256 fa243a3488464075e600cb9813df5aff27789997e133c9cc25de0bcd1082b3e3
MD5 d4560f5476fd7460cca71463db9e1bb5
BLAKE2b-256 b21634d5ee1854afc85e3c086177f1117577c3b2354e06d97dcfd11b640d0a28

See more details on using hashes here.

File details

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

File metadata

  • Download URL: required_dict-0.22-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.22-py3-none-any.whl
Algorithm Hash digest
SHA256 4b5fa13da53419dbc42c9b5d3c2ae236c824ca45fb36645cb5aef02ceef31b9c
MD5 7b22b48c15df590da5940c62bd655898
BLAKE2b-256 d36df0e3de53e695d727e6e3f31d8f6ea710ae067f6eef1ccef532dc7b1651f0

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