Skip to content

User friendly messages #1340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/errors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,69 @@ to guess the most relevant error in a given bunch.

.. autofunction:: by_relevance
:noindex:

Human-Friendly Error Messages
----------------------------

Sometimes the default validation error messages can be too technical for end users.
To help with this, jsonschema provides a way to convert validation errors into more
human-friendly messages.

.. testcode::

from jsonschema import human_validate

try:
human_validate({"age": "twenty"}, {"properties": {"age": {"type": "integer"}}})
except Exception as e:
print(e)

outputs:

.. testoutput::

Expected whole number, but got "twenty" at '$.age'

You can use these functions to work with human-friendly validation errors:

.. function:: human_validate(instance, schema, cls=None, *args, **kwargs)

Like :func:`jsonschema.validate`, but with human-friendly error messages.

.. function:: humanize_error(error)

Convert a ValidationError into a user-friendly message.

.. function:: enable_human_errors(validator_class)

Modify a validator class to use human-friendly error messages.

.. function:: create_human_validator(schema, *args, **kwargs)

Create a validator that uses human-friendly error messages.

.. function:: apply_to_all_validators()

Patch all validator classes to use human-friendly error messages.

For example, you could convert a technical error into a user-friendly one:

.. testcode::

from jsonschema import validate, humanize_error

schema = {"type": "object", "required": ["name", "email"]}
data = {"name": "John"}

try:
validate(data, schema)
except Exception as e:
print("Technical error:", e)
print("User-friendly error:", humanize_error(e))

outputs:

.. testoutput::

Technical error: 'email' is a required property
User-friendly error: Missing required field: 'email'
17 changes: 17 additions & 0 deletions jsonschema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,23 @@
Draft201909Validator,
Draft202012Validator,
validate,
human_validate,
)
from jsonschema.custom_validators import (
CustomValidator,
custom_validate,
)

# Provide a shortcut to the human-friendly error formatters for users
from jsonschema.human_errors import (
humanize_error,
create_human_validator,
enable_human_errors,
HumanValidationError,
apply_to_all_validators,
)

__version__ = "4.21.1.dev0"

def __getattr__(name):
if name == "__version__":
Expand Down Expand Up @@ -117,4 +132,6 @@ def __getattr__(name):
"TypeChecker",
"ValidationError",
"validate",
"CustomValidator",
"custom_validate",
]
94 changes: 94 additions & 0 deletions jsonschema/custom_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
Custom validator implementation that allows None values for any property and has special handling
for enum values and additionalProperties validation.
"""
from __future__ import annotations

from typing import Any, Dict, Iterator, Callable, List, Tuple, Union

from jsonschema import ValidationError, Draft7Validator, validators
from jsonschema.validators import extend


def extend_with_default(validator_class):
"""
Creates a custom validator that:
1. Allows None for any type, especially objects
2. Allows None for any enum property by default
3. Adds special handling for additionalProperties validation
4. Skips validation for missing or None properties
"""
validate_properties = validator_class.VALIDATORS["properties"]
validate_type = validator_class.VALIDATORS["type"]
validate_enum = validator_class.VALIDATORS.get("enum")
validate_additional_properties = validator_class.VALIDATORS.get("additionalProperties")

def set_defaults(validator, properties, instance, schema):
# Skip validation if instance is None
if instance is None:
return

for property, subschema in properties.items():
# If the property is missing in the instance, skip validation for it
if property not in instance or instance.get(property) is None:
continue
for error in validate_properties(
validator,
properties,
instance,
schema,
):
yield error

def ignore_none(validator, types, instance, schema):
# Allow None for any type, especially objects
if instance is None:
return
for error in validate_type(validator, types, instance, schema):
yield error

def enum_with_nullable(validator, enums, instance, schema):
# Allow None for any enum property by default
if instance is None:
return
if instance not in enums:
yield ValidationError(f"{instance} is not one of {enums}")

def validate_additional(validator, additional_properties, instance, schema):
# Ensure that instance is not None before iterating
if instance is None:
return
# Raise an error if additional properties are not allowed in the schema
if not additional_properties:
for property in instance:
if property not in schema.get("properties", {}):
yield ValidationError(f"Additional property '{property}' is not allowed.")

return validators.extend(
validator_class,
{
"properties": set_defaults,
"type": ignore_none,
"enum": enum_with_nullable,
"additionalProperties": validate_additional,
},
)


CustomValidator = extend_with_default(Draft7Validator)


def custom_validate(instance: Any, schema: Dict[str, Any]) -> None:
"""
Validate an instance against a schema using the custom validator that
allows None values for any property and has special handling for enum values
and additionalProperties validation.

Args:
instance: The instance to validate
schema: The schema to validate against

Raises:
ValidationError: If the instance is invalid
"""
CustomValidator(schema).validate(instance)
Loading
Loading