Files
privateGPT/tests/server/chat/test_schema_models.py
2026-07-16 13:36:11 +02:00

2432 lines
89 KiB
Python

import json
from typing import Any
import pytest
from pydantic import BaseModel, ValidationError
from private_gpt.chat.schema_models import create_model_from_json_schema
def test_array_schema_handling() -> None:
"""Test handling of array schemas at root level."""
# Test simple array of strings
string_array_schema = {
"type": "array",
"items": {"type": "string"},
"description": "Array of strings",
}
model = create_model_from_json_schema(string_array_schema, "StringArray")
assert issubclass(model, BaseModel)
# Test that model_json_schema returns the original schema
schema = model.model_json_schema()
assert schema == string_array_schema
# Test validation with array data
test_data = ["hello", "world", "test"]
instance = model.model_validate(test_data)
# Test that model_dump returns the array directly (not wrapped)
dumped = instance.model_dump()
assert dumped == test_data
assert isinstance(dumped, list)
# Test that model_dump_json returns JSON array directly
import json
json_str = instance.model_dump_json()
json_data = json.loads(json_str)
assert json_data == test_data
assert isinstance(json_data, list)
# Test array of objects with field sanitization
object_array_schema = {
"type": "array",
"items": {
"type": "object",
"properties": {
"_field": {"type": "string", "description": "Field with underscore"},
"class": {"type": "string", "description": "Keyword field"},
"normal": {"type": "number", "description": "Normal field"},
},
"required": ["_field"],
},
}
object_model = create_model_from_json_schema(object_array_schema, "ObjectArray")
assert issubclass(object_model, BaseModel)
# Test that model_json_schema returns the original schema for object arrays
object_schema = object_model.model_json_schema()
assert object_schema == object_array_schema
# Test with data that has field name issues
test_object_data = [
{"_field": "test1", "class": "MyClass", "normal": 1.5},
{"_field": "test2", "class": "YourClass", "normal": 2.5},
]
object_instance = object_model.model_validate(test_object_data)
# The result should be the original array with proper field names preserved
dumped_objects = object_instance.model_dump()
assert isinstance(dumped_objects, list)
assert len(dumped_objects) == 2
assert dumped_objects[0]["_field"] == "test1"
assert dumped_objects[0]["class"] == "MyClass"
assert dumped_objects[1]["_field"] == "test2"
# Test that nested objects have proper field sanitization
# The internal model should have sanitized field
# names but aliases preserve originals
item_instance = object_instance.items[0]
assert hasattr(
item_instance, "field"
) # _field becomes field (leading underscore removed)
assert hasattr(item_instance, "class_") # class becomes class_ (keyword sanitized)
# Test empty array
empty_instance = object_model.model_validate([])
empty_dumped = empty_instance.model_dump()
assert empty_dumped == []
assert isinstance(empty_dumped, list)
# Test an object that contains an array of objects (1 level)
object_array_schema = {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {"description": {"type": "string"}},
"required": ["description"],
},
}
},
"required": ["steps"],
}
object_array_model = create_model_from_json_schema(
object_array_schema, "ObjectWithArray"
)
assert issubclass(object_array_model, BaseModel)
# Test that model_json_schema returns the original schema for object with array
object_array_schema_result = object_array_model.model_json_schema()
assert object_array_schema_result == object_array_schema
@pytest.mark.parametrize(
"schema_case",
[
# Case 1: Root-level array with complex nested objects
{
"name": "root_array_complex_nested",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"user_id": {
"type": "integer",
"description": "User identifier",
},
"profile": {
"type": "object",
"properties": {
"personal_info": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0},
"contacts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"email",
"phone",
"address",
],
},
"value": {"type": "string"},
"is_primary": {
"type": "boolean",
"default": False,
},
},
"required": ["type", "value"],
},
},
},
"required": ["name", "age"],
},
"preferences": {
"type": "object",
"properties": {
"notifications": {
"type": "object",
"properties": {
"email": {
"type": "boolean",
"default": True,
},
"sms": {
"type": "boolean",
"default": False,
},
"push": {
"type": "boolean",
"default": True,
},
},
},
"privacy_settings": {
"type": "array",
"items": {
"type": "string",
"enum": [
"public",
"friends",
"private",
],
},
},
},
},
},
"required": ["personal_info"],
},
"metadata": {
"type": "object",
"properties": {
"created_at": {"type": "string", "format": "date-time"},
"tags": {"type": "array", "items": {"type": "string"}},
"scores": {
"type": "array",
"items": {"type": "number"},
},
"config": {
"type": "object",
"additionalProperties": True,
},
},
},
},
"required": ["user_id", "profile"],
},
"minItems": 1,
},
"test_data": [
{
"user_id": 12345,
"profile": {
"personal_info": {
"name": "John Doe",
"age": 30,
"contacts": [
{
"type": "email",
"value": "john@example.com",
"is_primary": True,
},
{"type": "phone", "value": "+1234567890"},
],
},
"preferences": {
"notifications": {
"email": True,
"sms": False,
"push": True,
},
"privacy_settings": ["friends", "private"],
},
},
"metadata": {
"created_at": "2024-01-15T10:30:00Z",
"tags": ["premium", "verified"],
"scores": [85.5, 92.0, 78.3],
"config": {"theme": "dark", "language": "en", "max_items": 100},
},
},
{
"user_id": 67890,
"profile": {
"personal_info": {
"name": "Jane Smith",
"age": 28,
"contacts": [
{"type": "email", "value": "jane@example.com"}
],
}
},
"metadata": {"tags": [], "scores": [95.2], "config": {}},
},
],
},
# Case 2: Complex object with multiple array types and field sanitization
{
"name": "complex_object_field_sanitization",
"schema": {
"type": "object",
"properties": {
"_id": {"type": "string", "description": "Document identifier"},
"class": {"type": "string", "description": "Document class"},
"def": {"type": "string", "description": "Definition field"},
"data_points": {
"type": "array",
"items": {
"type": "object",
"properties": {
"timestamp": {"type": "integer"},
"value": {"type": "number"},
"_metadata": {
"type": "object",
"properties": {
"source": {"type": "string"},
"quality": {
"type": "string",
"enum": ["high", "medium", "low"],
},
},
},
},
"required": ["timestamp", "value"],
},
},
"matrix": {
"type": "array",
"items": {"type": "array", "items": {"type": "number"}},
},
"nested_structure": {
"type": "object",
"properties": {
"level_1": {
"type": "object",
"properties": {
"level_2": {
"type": "object",
"properties": {
"level_3": {
"type": "array",
"items": {
"type": "object",
"properties": {
"from": {"type": "string"},
"to": {"type": "string"},
"weight": {"type": "number"},
},
},
}
},
}
},
}
},
},
},
"required": ["_id", "class", "data_points"],
},
"test_data": {
"_id": "doc_123",
"class": "sensor_data",
"def": "Temperature sensor readings",
"data_points": [
{
"timestamp": 1640995200,
"value": 23.5,
"_metadata": {"source": "sensor_01", "quality": "high"},
},
{
"timestamp": 1640995260,
"value": 24.1,
"_metadata": {"source": "sensor_01", "quality": "medium"},
},
],
"matrix": [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]],
"nested_structure": {
"level_1": {
"level_2": {
"level_3": [
{"from": "A", "to": "B", "weight": 0.8},
{"from": "B", "to": "C", "weight": 0.6},
]
}
}
},
},
},
# Case 3: Deeply nested object with circular-like references and complex arrays
{
"name": "deeply_nested_circular_like",
"schema": {
"type": "object",
"properties": {
"workflow": {
"type": "object",
"properties": {
"name": {"type": "string"},
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"step_id": {"type": "string"},
"action": {"type": "string"},
"parameters": {
"type": "object",
"additionalProperties": True,
},
"conditions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {"type": "string"},
"operator": {
"type": "string",
"enum": [
"eq",
"ne",
"gt",
"lt",
"in",
],
},
"value": {},
"nested_conditions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string"
},
"operator": {
"type": "string",
"enum": [
"eq",
"ne",
"gt",
"lt",
"in",
],
},
"value": {},
},
},
},
},
},
},
"outputs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"type": {"type": "string"},
"transformations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"function": {
"type": "string"
},
"params": {
"type": "array",
"items": {},
},
},
},
},
},
},
},
},
},
},
},
}
},
},
"test_data": {
"workflow": {
"name": "data_processing_pipeline",
"steps": [
{
"step_id": "extract",
"action": "extract_data",
"parameters": {
"source": "database",
"query": "SELECT * FROM users",
"timeout": 30,
},
"conditions": [
{
"field": "status",
"operator": "eq",
"value": "active",
"nested_conditions": [
{
"field": "last_login",
"operator": "gt",
"value": "2024-01-01",
}
],
}
],
"outputs": [
{
"name": "user_data",
"type": "dataframe",
"transformations": [
{
"function": "clean_nulls",
"params": ["name", "email"],
},
{"function": "validate_email", "params": []},
],
}
],
},
{
"step_id": "transform",
"action": "transform_data",
"parameters": {
"transformations": ["normalize", "encode"],
"encoding": "utf-8",
},
"conditions": [],
"outputs": [
{
"name": "processed_data",
"type": "array",
"transformations": [],
}
],
},
],
}
},
},
# Case 4: Tuple-style array items (items as array instead of object)
{
"name": "tuple_style_array_items",
"schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"_id": {"type": "string"},
"class": {"type": "string"},
},
"required": ["description"],
},
}
},
"required": ["steps"],
},
"test_data": {
"steps": [
{
"description": "First step description",
"_id": "step_1",
"class": "ProcessStep",
}
]
},
},
# Case 5: CLI-style tool schema with
# dash-prefixed fields and additionalProperties
{
"name": "cli_tool_dash_prefixed_fields",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern to search for",
},
"path": {
"type": "string",
"description": "File or directory to search in",
},
"output_mode": {
"type": "string",
"enum": ["content", "files_with_matches", "count"],
"description": "Output mode",
},
"-B": {
"type": "number",
"description": "Lines before match",
},
"-A": {
"type": "number",
"description": "Lines after match",
},
"-C": {
"type": "number",
"description": "Lines before and after match",
},
"-n": {
"type": "boolean",
"description": "Show line numbers",
},
"-i": {
"type": "boolean",
"description": "Case insensitive",
},
"head_limit": {
"type": "number",
"description": "Limit output to first N lines",
},
},
"required": ["pattern"],
"additionalProperties": False,
},
"test_data": {
"pattern": "log.*Error",
"path": "/var/log",
"output_mode": "content",
"-B": 2,
"-A": 3,
"-n": True,
"-i": False,
"head_limit": 100,
},
},
# Case 6: Tool schema with dash-prefixed fields and anyOf types
{
"name": "tool_schema_dash_fields_anyof",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command to execute",
},
"-v": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"default": None,
"description": "Verbose output",
},
"-o": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": None,
"description": "Output file path",
},
"-n": {
"anyOf": [{"type": "number"}, {"type": "null"}],
"default": None,
"description": "Number of results",
},
"format": {
"type": "string",
"enum": ["json", "yaml", "text", "csv"],
"description": "Output format",
},
"filters": {
"anyOf": [
{"type": "array", "items": {"type": "string"}},
{"type": "null"},
],
"default": None,
"description": "List of filter expressions",
},
},
"required": ["command", "format"],
"additionalProperties": False,
},
"test_data": {
"command": "list-resources",
"format": "json",
"-v": True,
"-o": "/tmp/output.json",
"-n": 50,
"filters": ["status=active", "region=us-east-1"],
},
},
# Case 7: Flat tool schema mixing enums, dash-flags,
# and additionalProperties: false
{
"name": "flat_tool_enum_and_dash_flags",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query string",
},
"index": {
"type": "string",
"description": "Index name to search",
},
"type": {
"type": "string",
"enum": ["exact", "fuzzy", "semantic", "hybrid"],
"description": "Search type",
},
"size": {
"type": "number",
"description": "Number of results to return",
},
"from": {
"type": "number",
"description": "Offset for pagination",
},
"-s": {
"type": "boolean",
"description": "Silent mode, suppress warnings",
},
"-p": {
"type": "boolean",
"description": "Pretty-print output",
},
"-r": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": None,
"description": "Remote endpoint override",
},
"fields": {
"anyOf": [
{"type": "array", "items": {"type": "string"}},
{"type": "null"},
],
"default": None,
"description": "Fields to include in response",
},
"sort": {
"anyOf": [
{
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {"type": "string"},
"order": {
"type": "string",
"enum": ["asc", "desc"],
},
},
"required": ["field", "order"],
},
},
{"type": "null"},
],
"default": None,
"description": "Sort criteria",
},
},
"required": ["query", "index", "type"],
"additionalProperties": False,
},
"test_data": {
"query": "error rate spike",
"index": "logs-2024",
"type": "hybrid",
"size": 25,
"from": 0,
"-s": False,
"-p": True,
"-r": None,
"fields": ["timestamp", "message", "level"],
"sort": [{"field": "timestamp", "order": "desc"}],
},
},
],
)
def test_complex_json_schema_handling(schema_case: dict[str, Any]) -> None:
"""Test handling of complex JSON schemas with various nested structures."""
schema = schema_case["schema"]
test_data = schema_case["test_data"]
case_name = schema_case["name"]
# Create model from schema
model = create_model_from_json_schema(schema, f"ComplexModel_{case_name}")
assert issubclass(model, BaseModel)
# Test that model_json_schema returns the original schema
generated_schema = model.model_json_schema()
assert generated_schema == schema, f"Schema mismatch for case: {case_name}"
# Test validation with test data
instance = model.model_validate(test_data)
assert instance is not None
# Test model_dump returns data in expected format
dumped = instance.model_dump()
if schema["type"] == "array":
# For root-level arrays, dumped should be a list
assert isinstance(dumped, list), (
f"Expected list for array schema in case: {case_name}"
)
assert dumped == test_data, f"Data mismatch for case: {case_name}"
else:
# For objects, dumped should be a dict
assert isinstance(dumped, dict), (
f"Expected dict for object schema in case: {case_name}"
)
# Verify all required fields are present and correctly mapped
_verify_data_integrity(dumped, test_data, case_name)
# Test JSON serialization/deserialization
json_str = instance.model_dump_json()
json_data = json.loads(json_str)
if schema["type"] == "array":
assert isinstance(json_data, list), (
f"JSON should be array for case: {case_name}"
)
else:
assert isinstance(json_data, dict), (
f"JSON should be object for case: {case_name}"
)
# Test that we can create a new instance from dumped data
new_instance = model.model_validate(dumped)
assert new_instance.model_dump() == dumped, (
f"Round-trip validation failed for case: {case_name}"
)
# Test field sanitization for object schemas
if schema["type"] == "object":
_verify_field_sanitization(instance, schema, case_name)
def _verify_data_integrity(
dumped: dict[str, Any], original: dict[str, Any], case_name: str
) -> None:
"""Verify that dumped data maintains integrity with original data."""
# Check that all original keys are preserved in dumped data
for key, value in original.items():
assert key in dumped, (
f"Key '{key}' missing in dumped data for case: {case_name}"
)
if isinstance(value, dict):
assert isinstance(dumped[key], dict), (
f"Value type mismatch for key '{key}' in case: {case_name}"
)
_verify_data_integrity(dumped[key], value, case_name)
elif isinstance(value, list):
assert isinstance(dumped[key], list), (
f"Value type mismatch for key '{key}' in case: {case_name}"
)
assert len(dumped[key]) == len(value), (
f"Array length mismatch for key '{key}' in case: {case_name}"
)
else:
assert dumped[key] == value, (
f"Value mismatch for key '{key}' in case: {case_name}"
)
def _verify_field_sanitization(
instance: BaseModel, schema: dict[str, Any], case_name: str
) -> None:
"""Verify that field names are properly sanitized while maintaining aliases."""
if "properties" in schema:
for field_name in schema["properties"]:
# Check for fields that should be sanitized
if field_name.startswith("_"):
sanitized_name = field_name.lstrip("_")
if sanitized_name: # Only if there's something left after stripping
assert hasattr(instance, sanitized_name), (
f"Sanitized field '{sanitized_name}' not found for case: {case_name}"
)
elif field_name in [
"class",
"def",
"from",
"to",
"if",
"else",
"for",
"while",
"return",
"import",
]:
# Python keywords should be sanitized
sanitized_name = f"{field_name}_"
assert hasattr(instance, sanitized_name), (
f"Sanitized keyword field '{sanitized_name}' not found for case: {case_name}"
)
def test_tuple_style_array_items_raises_error() -> None:
"""Test that tuple-style array items (items as array) raise ValueError."""
# Schema with tuple-style items (items is an array) - should crash
tuple_array_schema = {
"type": "array",
"items": [
{
"type": "object",
"properties": {"description": {"type": "string"}},
"required": ["description"],
}
],
}
# This should raise ValueError
with pytest.raises(ValueError):
create_model_from_json_schema(tuple_array_schema, "TupleArrayModel")
def test_nested_tuple_style_array_items_raises_error() -> None:
"""Test that nested tuple-style array items also raise ValueError."""
# Object schema with nested array that has tuple-style items
nested_tuple_schema = {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": [
{
"type": "object",
"properties": {"description": {"type": "string"}},
"required": ["description"],
}
],
}
},
"required": ["steps"],
}
# This should also raise ValueError when processing the nested array
with pytest.raises(ValueError):
create_model_from_json_schema(nested_tuple_schema, "NestedTupleModel")
def test_object_schema_backward_compatibility() -> None:
"""Test that object schemas still work as before."""
object_schema = {
"type": "object",
"properties": {
"param1": {"type": "string", "description": "First parameter"},
"_param2": {
"type": "number",
"description": "Second parameter with underscore",
},
"normal_param": {"type": "boolean", "description": "Normal parameter"},
"class": {"type": "string", "description": "Keyword field"},
"match": {"type": "string", "description": "Soft keyword field"},
},
"required": ["param1"],
}
model = create_model_from_json_schema(object_schema, "TestModel")
assert issubclass(model, BaseModel)
# Create instance with original field names (using aliases)
instance = model(
param1="test_value",
param2=42.5, # sanitized field name
normal_param=True,
class_="test_class", # keyword sanitized
match_="test_match", # soft keyword sanitized
)
# Test default serialization returns original field names
serialized = instance.model_dump()
# Should contain original field names with underscores by default
assert "param1" in serialized
assert "_param2" in serialized
assert "normal_param" in serialized
assert "class" in serialized
assert "match" in serialized
# Values should be preserved
assert serialized["param1"] == "test_value"
assert serialized["_param2"] == 42.5
assert serialized["normal_param"] is True
assert serialized["class"] == "test_class"
assert serialized["match"] == "test_match"
# Test JSON serialization also uses original names by default
import json
json_str = instance.model_dump_json()
json_data = json.loads(json_str)
assert "_param2" in json_data
assert json_data["_param2"] == 42.5
assert "class" in json_data
assert "match" in json_data
def test_anyof_with_null_optional_field() -> None:
"""Test handling of anyOf with null type (your specific case)."""
schema = {
"properties": {
"a": {
"anyOf": [{"type": "number"}, {"type": "null"}],
"default": None,
"description": "The first number to multiply",
"title": "A",
},
"b": {
"anyOf": [{"type": "number"}, {"type": "null"}],
"default": None,
"description": "The second number to multiply",
"title": "B",
},
},
"title": "multiply_Schema",
"type": "object",
}
model = create_model_from_json_schema(schema, "MultiplySchema")
assert issubclass(model, BaseModel)
# Test with both values provided
instance1 = model.model_validate({"a": 5.0, "b": 10.0})
assert instance1.a == 5.0
assert instance1.b == 10.0
# Test with null values
instance2 = model.model_validate({"a": None, "b": None})
assert instance2.a is None
assert instance2.b is None
# Test with mixed values
instance3 = model.model_validate({"a": 3.14, "b": None})
assert instance3.a == 3.14
assert instance3.b is None
# Test with missing values (should use defaults)
instance4 = model.model_validate({})
assert instance4.a is None
assert instance4.b is None
# Test serialization preserves original field names
dumped = instance1.model_dump()
assert dumped == {"a": 5.0, "b": 10.0}
def test_anyof_multiple_types() -> None:
"""Test anyOf with multiple non-null types."""
schema = {
"type": "object",
"properties": {
"value": {
"anyOf": [{"type": "string"}, {"type": "number"}, {"type": "boolean"}],
"description": "Can be string, number, or boolean",
}
},
"required": ["value"],
}
model = create_model_from_json_schema(schema, "AnyOfModel")
# Test with string
instance1 = model.model_validate({"value": "test"})
assert instance1.value == "test"
# Test with number
instance2 = model.model_validate({"value": 42})
assert instance2.value == 42
# Test with boolean
instance3 = model.model_validate({"value": True})
assert instance3.value is True
def test_anyof_with_array() -> None:
"""Test anyOf containing array type."""
schema = {
"type": "object",
"properties": {
"data": {
"anyOf": [
{"type": "array", "items": {"type": "string"}},
{"type": "null"},
],
"description": "Optional array of strings",
}
},
}
model = create_model_from_json_schema(schema, "AnyOfArrayModel")
# Test with array
instance1 = model.model_validate({"data": ["a", "b", "c"]})
assert instance1.data == ["a", "b", "c"]
# Test with null
instance2 = model.model_validate({"data": None})
assert instance2.data is None
# Test with missing (should be None)
instance3 = model.model_validate({})
assert instance3.data is None
def test_oneof_handling() -> None:
"""Test oneOf schema handling."""
schema = {
"type": "object",
"properties": {
"id": {
"oneOf": [{"type": "string"}, {"type": "integer"}],
"description": "ID can be string or integer",
}
},
"required": ["id"],
}
model = create_model_from_json_schema(schema, "OneOfModel")
# Test with string
instance1 = model.model_validate({"id": "abc123"})
assert instance1.id == "abc123"
# Test with integer
instance2 = model.model_validate({"id": 123})
assert instance2.id == 123
def test_allof_handling() -> None:
"""Test allOf schema handling - takes first type."""
schema = {
"type": "object",
"properties": {
"name": {
"allOf": [
{"type": "string", "minLength": 1},
{"type": "string", "maxLength": 100},
],
"description": "Name with constraints",
}
},
"required": ["name"],
}
model = create_model_from_json_schema(schema, "AllOfModel")
# Should accept string
instance = model.model_validate({"name": "John Doe"})
assert instance.name == "John Doe"
def test_nested_anyof_in_array() -> None:
"""Test anyOf inside array items."""
schema = {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"anyOf": [{"type": "string"}, {"type": "number"}],
},
}
},
}
model = create_model_from_json_schema(schema, "NestedAnyOfModel")
# Test with mixed types in array
instance = model.model_validate({"items": ["test", 42, "another", 3.14]})
assert instance.items == ["test", 42, "another", 3.14]
def test_anyof_with_object_types() -> None:
"""Test anyOf with different object schemas."""
schema = {
"type": "object",
"properties": {
"response": {
"anyOf": [
{
"type": "object",
"properties": {"success": {"type": "boolean"}},
},
{
"type": "object",
"properties": {"error": {"type": "string"}},
},
],
}
},
}
model = create_model_from_json_schema(schema, "AnyOfObjectModel")
# Test with first schema variant
instance1 = model.model_validate({"response": {"success": True}})
assert instance1.response == {"success": True}
# Test with second schema variant
instance2 = model.model_validate({"response": {"error": "Failed"}})
assert instance2.response == {"error": "Failed"}
def test_complex_anyof_with_required_and_optional() -> None:
"""Test complex schema with anyOf in both required and optional fields."""
schema = {
"type": "object",
"properties": {
"required_field": {
"anyOf": [{"type": "string"}, {"type": "integer"}],
"description": "Required field with union type",
},
"optional_field": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"default": None,
"description": "Optional field with null",
},
"another_optional": {
"anyOf": [
{"type": "array", "items": {"type": "number"}},
{"type": "null"},
],
"description": "Optional array",
},
},
"required": ["required_field"],
}
model = create_model_from_json_schema(schema, "ComplexAnyOfModel")
# Test with all fields
instance1 = model.model_validate(
{
"required_field": "test",
"optional_field": True,
"another_optional": [1.5, 2.5],
}
)
assert instance1.required_field == "test"
assert instance1.optional_field is True
assert instance1.another_optional == [1.5, 2.5]
# Test with only required field
instance2 = model.model_validate({"required_field": 42})
assert instance2.required_field == 42
assert instance2.optional_field is None
assert instance2.another_optional is None
# Test with null optional fields
instance3 = model.model_validate(
{"required_field": "value", "optional_field": None, "another_optional": None}
)
assert instance3.required_field == "value"
assert instance3.optional_field is None
assert instance3.another_optional is None
def test_anyof_with_field_sanitization() -> None:
"""Test anyOf works correctly with field name sanitization."""
schema = {
"type": "object",
"properties": {
"_field": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": None,
"description": "Field with underscore",
},
"class": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Python keyword field",
},
},
"required": ["class"],
}
model = create_model_from_json_schema(schema, "SanitizedAnyOfModel")
# Test that fields are accessible with sanitized names
instance = model.model_validate({"_field": "test", "class": 42})
assert hasattr(instance, "field") # _field becomes field
assert hasattr(instance, "class_") # class becomes class_
# Test serialization uses original names
dumped = instance.model_dump()
assert "_field" in dumped
assert "class" in dumped
assert dumped["_field"] == "test"
assert dumped["class"] == 42
def test_root_level_array_with_anyof_items() -> None:
"""Test root-level array where items use anyOf."""
schema = {
"type": "array",
"items": {
"anyOf": [{"type": "string"}, {"type": "number"}, {"type": "null"}],
},
}
model = create_model_from_json_schema(schema, "ArrayAnyOfModel")
# Test with mixed types
test_data = ["hello", 42, None, "world", 3.14]
instance = model.model_validate(test_data)
dumped = instance.model_dump()
assert dumped == test_data
def test_nested_anyof_in_object_in_array() -> None:
"""Test complex nesting: array of objects with anyOf fields."""
schema = {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"anyOf": [{"type": "string"}, {"type": "integer"}],
},
"value": {
"anyOf": [{"type": "number"}, {"type": "null"}],
"default": None,
},
},
"required": ["id"],
},
}
model = create_model_from_json_schema(schema, "ComplexNestedAnyOfModel")
test_data = [
{"id": "abc", "value": 10.5},
{"id": 123, "value": None},
{"id": "xyz", "value": 42.0},
]
instance = model.model_validate(test_data)
dumped = instance.model_dump()
assert len(dumped) == 3
assert dumped[0]["id"] == "abc"
assert dumped[0]["value"] == 10.5
assert dumped[1]["id"] == 123
assert dumped[1]["value"] is None
assert dumped[2]["id"] == "xyz"
assert dumped[2]["value"] == 42.0
def test_oneof_with_null() -> None:
"""Test oneOf including null type."""
schema = {
"type": "object",
"properties": {
"status": {
"oneOf": [
{"type": "string", "enum": ["active", "inactive"]},
{"type": "null"},
],
}
},
}
model = create_model_from_json_schema(schema, "OneOfNullModel")
# Test with string
instance1 = model.model_validate({"status": "active"})
assert instance1.status == "active"
# Test with null
instance2 = model.model_validate({"status": None})
assert instance2.status is None
def test_allof_with_multiple_schemas() -> None:
"""Test allOf with multiple schemas - uses first with type."""
schema = {
"type": "object",
"properties": {
"email": {
"allOf": [
{"type": "string"},
{"format": "email"},
{"minLength": 5},
],
}
},
"required": ["email"],
}
model = create_model_from_json_schema(schema, "AllOfEmailModel")
instance = model.model_validate({"email": "test@example.com"})
assert instance.email == "test@example.com"
def test_mixed_combinators() -> None:
"""Test schema with multiple combinator types (anyOf, allOf, oneOf)."""
schema = {
"type": "object",
"properties": {
"field1": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"default": None,
},
"field2": {
"oneOf": [{"type": "integer"}, {"type": "boolean"}],
},
"field3": {
"allOf": [{"type": "string"}, {"minLength": 1}],
},
},
"required": ["field2", "field3"],
}
model = create_model_from_json_schema(schema, "MixedCombinatorsModel")
instance = model.model_validate({"field1": "test", "field2": 42, "field3": "hello"})
assert instance.field1 == "test"
assert instance.field2 == 42
assert instance.field3 == "hello"
# Test with null for field1
instance2 = model.model_validate({"field1": None, "field2": True, "field3": "hi"})
assert instance2.field1 is None
assert instance2.field2 is True
assert instance2.field3 == "hi"
def test_anyof_preserves_schema() -> None:
"""Test that model_json_schema returns original schema with anyOf."""
schema = {
"type": "object",
"properties": {
"value": {
"anyOf": [{"type": "number"}, {"type": "null"}],
"default": None,
}
},
}
model = create_model_from_json_schema(schema, "PreserveSchemaModel")
returned_schema = model.model_json_schema()
assert returned_schema == schema
def test_deeply_nested_anyof() -> None:
"""Test anyOf deeply nested in complex structure."""
schema = {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"nested": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"anyOf": [
{"type": "string"},
{"type": "number"},
{"type": "null"},
],
}
},
},
}
},
}
},
}
model = create_model_from_json_schema(schema, "DeeplyNestedAnyOfModel")
test_data = {
"data": {
"nested": [
{"value": "string"},
{"value": 42},
{"value": None},
{"value": 3.14},
]
}
}
instance = model.model_validate(test_data)
dumped = instance.model_dump()
assert dumped["data"]["nested"][0]["value"] == "string"
assert dumped["data"]["nested"][1]["value"] == 42
assert dumped["data"]["nested"][2]["value"] is None
assert dumped["data"]["nested"][3]["value"] == 3.14
def test_json_serialization_with_anyof() -> None:
"""Test JSON serialization/deserialization with anyOf fields."""
schema = {
"type": "object",
"properties": {
"id": {
"anyOf": [{"type": "string"}, {"type": "integer"}],
},
"optional": {
"anyOf": [{"type": "number"}, {"type": "null"}],
"default": None,
},
},
"required": ["id"],
}
model = create_model_from_json_schema(schema, "JsonSerializationModel")
# Create instance and serialize to JSON
instance = model.model_validate({"id": "abc123", "optional": 42.5})
json_str = instance.model_dump_json()
json_data = json.loads(json_str)
assert json_data["id"] == "abc123"
assert json_data["optional"] == 42.5
# Test round-trip
new_instance = model.model_validate(json_data)
assert new_instance.id == "abc123"
assert new_instance.optional == 42.5
class TestComplexNestedSchemas:
"""Test deeply nested and complex object structures."""
def test_deeply_nested_objects(self) -> None:
"""Test schema with multiple levels of nested objects."""
schema = {
"type": "object",
"properties": {
"level1": {
"type": "object",
"properties": {
"level2": {
"type": "object",
"properties": {
"level3": {
"type": "object",
"properties": {"deep_value": {"type": "string"}},
"required": ["deep_value"],
}
},
"required": ["level3"],
}
},
"required": ["level2"],
}
},
"required": ["level1"],
}
Model = create_model_from_json_schema(schema, "DeeplyNestedModel")
# Valid nested data
data = {"level1": {"level2": {"level3": {"deep_value": "found it!"}}}}
instance = Model.model_validate(data)
assert instance.level1["level2"]["level3"]["deep_value"] == "found it!"
def test_mixed_nested_arrays_and_objects(self) -> None:
"""Test schema mixing nested arrays and objects."""
schema = {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"addresses": {
"type": "array",
"items": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"},
"coordinates": {
"type": "array",
"items": {"type": "number"},
},
},
"required": ["street", "city"],
},
},
},
"required": ["name"],
},
}
},
"required": ["users"],
}
Model = create_model_from_json_schema(schema, "MixedNestedModel")
data = {
"users": [
{
"name": "Alice",
"addresses": [
{
"street": "123 Main St",
"city": "NYC",
"coordinates": [40.7128, -74.0060],
}
],
},
{"name": "Bob", "addresses": []},
]
}
instance = Model.model_validate(data)
assert len(instance.users) == 2
assert instance.users[0]["name"] == "Alice"
assert instance.users[0]["addresses"][0]["coordinates"] == [40.7128, -74.0060]
class TestComplexUnionTypes:
"""Test complex union type scenarios with anyOf, oneOf, allOf."""
def test_anyof_with_multiple_complex_types(self) -> None:
"""Test anyOf with multiple object types."""
schema = {
"type": "object",
"properties": {
"data": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {"type": "string"},
"value": {"type": "string"},
},
"required": ["type", "value"],
},
{
"type": "object",
"properties": {
"type": {"type": "string"},
"count": {"type": "integer"},
},
"required": ["type", "count"],
},
{"type": "array", "items": {"type": "string"}},
{"type": "null"},
]
}
},
"required": [],
}
Model = create_model_from_json_schema(schema, "AnyOfComplexModel")
# Test with first type
instance1 = Model.model_validate({"data": {"type": "text", "value": "hello"}})
assert instance1.data["type"] == "text"
# Test with array type
instance2 = Model.model_validate({"data": ["a", "b", "c"]})
assert instance2.data == ["a", "b", "c"]
# Test with null
instance3 = Model.model_validate({"data": None})
assert instance3.data is None
def test_oneof_with_discriminator_like_pattern(self) -> None:
"""Test oneOf simulating discriminator pattern."""
schema = {
"type": "object",
"properties": {
"shape": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": {"type": "string"},
"radius": {"type": "number"},
},
"required": ["kind", "radius"],
},
{
"type": "object",
"properties": {
"kind": {"type": "string"},
"width": {"type": "number"},
"height": {"type": "number"},
},
"required": ["kind", "width", "height"],
},
]
}
},
"required": ["shape"],
}
Model = create_model_from_json_schema(schema, "OneOfDiscriminatorModel")
circle = Model.model_validate({"shape": {"kind": "circle", "radius": 5.0}})
assert circle.shape["radius"] == 5.0
rectangle = Model.model_validate(
{"shape": {"kind": "rectangle", "width": 10, "height": 20}}
)
assert rectangle.shape["width"] == 10
def test_allof_composition(self) -> None:
"""Test allOf schema composition."""
schema = {
"type": "object",
"properties": {
"entity": {
"allOf": [
{
"type": "object",
"properties": {
"id": {"type": "string"},
"created_at": {"type": "string"},
},
},
{
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
},
},
]
}
},
"required": [],
}
Model = create_model_from_json_schema(schema, "AllOfCompositionModel")
data = {
"entity": {
"id": "123",
"created_at": "2025-01-01",
"name": "John",
"email": "john@example.com",
}
}
instance = Model.model_validate(data)
assert instance.entity["id"] == "123"
assert instance.entity["name"] == "John"
def test_nested_anyof_in_array(self) -> None:
"""Test anyOf within array items."""
schema = {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"anyOf": [
{"type": "string"},
{"type": "number"},
{
"type": "object",
"properties": {"nested": {"type": "boolean"}},
},
]
},
}
},
"required": [],
}
Model = create_model_from_json_schema(schema, "NestedAnyOfArrayModel")
data = {"items": ["string_value", 42, 3.14, {"nested": True}, "another_string"]}
instance = Model.model_validate(data)
assert len(instance.items) == 5
assert instance.items[0] == "string_value"
assert instance.items[1] == 42
assert instance.items[3]["nested"] is True
class TestReservedNamesAndSanitization:
"""Test handling of reserved names and field sanitization."""
def test_all_pydantic_reserved_names(self) -> None:
"""Test all Pydantic reserved names are properly sanitized."""
reserved_properties = {
"model_config": {"type": "string"},
"model_fields": {"type": "string"},
"model_computed_fields": {"type": "string"},
"model_dump": {"type": "string"},
"model_validate": {"type": "string"},
"dict": {"type": "string"},
"json": {"type": "string"},
"copy": {"type": "string"},
"schema": {"type": "string"},
}
schema = {"type": "object", "properties": reserved_properties, "required": []}
Model = create_model_from_json_schema(schema, "ReservedNamesModel")
data = {name: f"value_{name}" for name in reserved_properties}
instance = Model.model_validate(data)
# Verify all fields are accessible and have correct values
dumped = instance.model_dump(by_alias=True)
for name in reserved_properties:
assert dumped[name] == f"value_{name}"
def test_python_keywords_as_fields(self) -> None:
"""Test Python keywords are properly handled."""
schema = {
"type": "object",
"properties": {
"class": {"type": "string"},
"def": {"type": "string"},
"return": {"type": "string"},
"if": {"type": "string"},
"else": {"type": "string"},
"import": {"type": "string"},
"from": {"type": "string"},
"as": {"type": "string"},
"try": {"type": "string"},
"except": {"type": "string"},
},
"required": ["class", "def"],
}
Model = create_model_from_json_schema(schema, "KeywordsModel")
data = {
"class": "MyClass",
"def": "my_function",
"return": "value",
"if": "condition",
}
instance = Model.model_validate(data)
dumped = instance.model_dump(by_alias=True)
assert dumped["class"] == "MyClass"
assert dumped["def"] == "my_function"
def test_invalid_identifier_characters(self) -> None:
"""Test fields with invalid Python identifier characters."""
schema = {
"type": "object",
"properties": {
"field-with-dashes": {"type": "string"},
"field.with.dots": {"type": "string"},
"field with spaces": {"type": "string"},
"field@with#special$chars": {"type": "string"},
"123numeric_start": {"type": "string"},
"field/slash": {"type": "string"},
},
"required": [],
}
Model = create_model_from_json_schema(schema, "InvalidCharsModel")
data = {
"field-with-dashes": "value1",
"field.with.dots": "value2",
"field with spaces": "value3",
"field@with#special$chars": "value4",
"123numeric_start": "value5",
"field/slash": "value6",
}
instance = Model.model_validate(data)
dumped = instance.model_dump(by_alias=True)
assert dumped["field-with-dashes"] == "value1"
assert dumped["field.with.dots"] == "value2"
assert dumped["field with spaces"] == "value3"
assert dumped["123numeric_start"] == "value5"
def test_dunder_names(self) -> None:
"""Test double underscore (dunder) names."""
schema = {
"type": "object",
"properties": {
"__init__": {"type": "string"},
"__name__": {"type": "string"},
"__dict__": {"type": "string"},
},
"required": [],
}
Model = create_model_from_json_schema(schema, "DunderNamesModel")
data = {
"__init__": "value1",
"__name__": "value2",
"__dict__": "value3",
}
instance = Model.model_validate(data)
dumped = instance.model_dump(by_alias=True)
assert dumped["__init__"] == "value1"
def test_collision_resolution(self) -> None:
"""Test field name collision resolution."""
schema = {
"type": "object",
"properties": {
"field": {"type": "string"},
"field_": {"type": "string"},
"class": {"type": "string"}, # becomes class_
"class_": {"type": "string"}, # collision with sanitized class
},
"required": [],
}
Model = create_model_from_json_schema(schema, "CollisionModel")
data = {
"field": "value1",
"field_": "value2",
"class": "value3",
"class_": "value4",
}
instance = Model.model_validate(data)
dumped = instance.model_dump(by_alias=True)
assert dumped["field"] == "value1"
assert dumped["field_"] == "value2"
assert dumped["class"] == "value3"
assert dumped["class_"] == "value4"
class TestComplexArraySchemas:
"""Test complex array schema scenarios."""
def test_root_level_array_with_nested_objects(self) -> None:
"""Test array schema at root with complex nested objects."""
schema = {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"metadata": {
"type": "object",
"properties": {
"tags": {"type": "array", "items": {"type": "string"}},
"attributes": {
"type": "object",
"properties": {
"key": {"type": "string"},
"value": {"type": "string"},
},
},
},
},
},
"required": ["id"],
},
}
Model = create_model_from_json_schema(schema, "RootArrayNestedModel")
data = [
{
"id": 1,
"metadata": {
"tags": ["tag1", "tag2"],
"attributes": {"key": "color", "value": "blue"},
},
},
{
"id": 2,
"metadata": {
"tags": [],
"attributes": {"key": "size", "value": "large"},
},
},
]
instance = Model.model_validate(data)
dumped = instance.model_dump()
assert len(dumped) == 2
assert dumped[0]["id"] == 1
assert dumped[0]["metadata"]["tags"] == ["tag1", "tag2"]
assert dumped[1]["metadata"]["attributes"]["value"] == "large"
def test_root_array_json_serialization(self) -> None:
"""Test that root-level arrays serialize correctly to JSON."""
schema = {
"type": "array",
"items": {
"type": "object",
"properties": {"name": {"type": "string"}, "value": {"type": "number"}},
"required": ["name", "value"],
},
}
Model = create_model_from_json_schema(schema, "RootArrayJsonModel")
data = [{"name": "item1", "value": 10.5}, {"name": "item2", "value": 20.3}]
instance = Model.model_validate(data)
json_str = instance.model_dump_json()
parsed = json.loads(json_str)
assert isinstance(parsed, list)
assert len(parsed) == 2
assert parsed[0]["name"] == "item1"
def test_array_of_arrays(self) -> None:
"""Test nested array structures."""
schema = {
"type": "object",
"properties": {
"matrix": {
"type": "array",
"items": {"type": "array", "items": {"type": "number"}},
}
},
"required": ["matrix"],
}
Model = create_model_from_json_schema(schema, "ArrayOfArraysModel")
data = {"matrix": [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}
instance = Model.model_validate(data)
assert instance.matrix[1][1] == 5
def test_array_with_anyof_items(self) -> None:
"""Test arrays where items can be different types."""
schema = {
"type": "array",
"items": {
"anyOf": [
{"type": "string"},
{"type": "number"},
{
"type": "object",
"properties": {
"type": {"type": "string"},
"data": {"anyOf": [{"type": "string"}, {"type": "number"}]},
},
},
]
},
}
Model = create_model_from_json_schema(schema, "HeterogeneousArrayModel")
data = [
"string",
42,
3.14,
{"type": "custom", "data": "value"},
{"type": "other", "data": 100},
]
instance = Model.model_validate(data)
dumped = instance.model_dump()
assert dumped[0] == "string"
assert dumped[1] == 42
assert dumped[3]["type"] == "custom"
class TestEdgeCasesAndValidation:
"""Test edge cases and validation scenarios."""
def test_empty_schema(self) -> None:
"""Test handling of empty schema."""
schema: dict[str, Any] = {}
Model = create_model_from_json_schema(schema, "EmptySchemaModel")
instance = Model.model_validate({})
assert instance.model_dump() == {}
def test_schema_with_no_properties(self) -> None:
"""Test object schema with no properties."""
schema = {"type": "object", "properties": {}, "required": []}
Model = create_model_from_json_schema(schema, "NoPropsModel")
instance = Model.model_validate({})
assert instance.model_dump() == {}
def test_all_optional_fields_with_defaults(self) -> None:
"""Test schema where all fields are optional with defaults."""
schema = {
"type": "object",
"properties": {
"str_field": {"type": "string", "default": "default_string"},
"int_field": {"type": "integer", "default": 42},
"bool_field": {"type": "boolean", "default": True},
"array_field": {
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
"required": [],
}
Model = create_model_from_json_schema(schema, "DefaultsModel")
# Create with no data - should use defaults
instance = Model.model_validate({})
dumped = instance.model_dump(exclude_defaults=False)
assert dumped["str_field"] == "default_string"
assert dumped["int_field"] == 42
assert dumped["bool_field"] is True
assert dumped["array_field"] == []
def test_required_fields_validation(self) -> None:
"""Test that required fields are actually enforced."""
schema = {
"type": "object",
"properties": {
"required_field": {"type": "string"},
"optional_field": {"type": "string"},
},
"required": ["required_field"],
}
Model = create_model_from_json_schema(schema, "RequiredValidationModel")
# Should succeed with required field
instance = Model.model_validate({"required_field": "value"})
assert instance.required_field == "value"
# Should fail without required field
with pytest.raises(ValidationError):
Model.model_validate({})
with pytest.raises(ValidationError):
Model.model_validate({"optional_field": "value"})
def test_type_validation_enforcement(self) -> None:
"""Test that type validation is enforced."""
schema = {
"type": "object",
"properties": {
"str_field": {"type": "string"},
"int_field": {"type": "integer"},
"bool_field": {"type": "boolean"},
},
"required": ["str_field", "int_field"],
}
Model = create_model_from_json_schema(schema, "TypeValidationModel")
# Valid data
instance = Model.model_validate(
{"str_field": "text", "int_field": 42, "bool_field": True}
)
assert instance.str_field == "text"
# Invalid types should raise validation errors
with pytest.raises(ValidationError):
Model.model_validate(
{"str_field": 123, "int_field": 42} # Should be string
)
def test_nullable_fields_with_anyof(self) -> None:
"""Test fields that can be null using anyOf."""
schema = {
"type": "object",
"properties": {
"nullable_string": {"anyOf": [{"type": "string"}, {"type": "null"}]},
"nullable_object": {
"anyOf": [
{"type": "object", "properties": {"value": {"type": "number"}}},
{"type": "null"},
]
},
},
"required": ["nullable_string"],
}
Model = create_model_from_json_schema(schema, "NullableFieldsModel")
# Test with null values
instance1 = Model.model_validate(
{"nullable_string": None, "nullable_object": None}
)
assert instance1.nullable_string is None
assert instance1.nullable_object is None
# Test with actual values
instance2 = Model.model_validate(
{"nullable_string": "value", "nullable_object": {"value": 42.5}}
)
assert instance2.nullable_string == "value"
assert instance2.nullable_object["value"] == 42.5
class TestRealWorldSchemas:
"""Test real-world complex schema patterns."""
def test_openapi_style_response_schema(self) -> None:
"""Test OpenAPI-style response schema with nested references."""
schema = {
"type": "object",
"properties": {
"status": {"type": "string"},
"data": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"username": {"type": "string"},
"email": {"type": "string"},
"profile": {
"type": "object",
"properties": {
"bio": {"type": "string"},
"avatar_url": {"type": "string"},
"social_links": {
"type": "array",
"items": {"type": "string"},
},
},
},
"is_active": {"type": "boolean"},
},
"required": ["id", "username", "email"],
},
},
"pagination": {
"type": "object",
"properties": {
"page": {"type": "integer"},
"per_page": {"type": "integer"},
"total": {"type": "integer"},
},
"required": ["page", "per_page", "total"],
},
},
"required": ["users"],
},
"meta": {
"type": "object",
"properties": {
"timestamp": {"type": "string"},
"request_id": {"type": "string"},
},
},
},
"required": ["status", "data"],
}
Model = create_model_from_json_schema(schema, "APIResponseModel")
data = {
"status": "success",
"data": {
"users": [
{
"id": "usr_123",
"username": "alice",
"email": "alice@example.com",
"profile": {
"bio": "Software Engineer",
"avatar_url": "https://example.com/avatar.jpg",
"social_links": ["https://twitter.com/alice"],
},
"is_active": True,
}
],
"pagination": {"page": 1, "per_page": 10, "total": 100},
},
"meta": {"timestamp": "2025-01-01T00:00:00Z", "request_id": "req_abc123"},
}
instance = Model.model_validate(data)
assert instance.status == "success"
assert instance.data["users"][0]["username"] == "alice"
assert instance.data["pagination"]["total"] == 100
def test_configuration_schema_with_mixed_types(self) -> None:
"""Test configuration-style schema with various type combinations."""
schema = {
"type": "object",
"properties": {
"version": {"type": "string"},
"database": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"},
"credentials": {
"anyOf": [
{
"type": "object",
"properties": {
"username": {"type": "string"},
"password": {"type": "string"},
},
"required": ["username", "password"],
},
{
"type": "object",
"properties": {"token": {"type": "string"}},
"required": ["token"],
},
]
},
"options": {
"type": "object",
"properties": {
"ssl": {"type": "boolean"},
"timeout": {"type": "integer"},
"retry_attempts": {"type": "integer"},
},
},
},
"required": ["host", "port"],
},
"features": {"type": "array", "items": {"type": "string"}},
"limits": {
"type": "object",
"properties": {
"max_connections": {"type": "integer"},
"rate_limit": {
"anyOf": [{"type": "integer"}, {"type": "null"}]
},
},
},
},
"required": ["version", "database"],
}
Model = create_model_from_json_schema(schema, "ConfigurationModel")
# Test with username/password credentials
config1 = {
"version": "1.0.0",
"database": {
"host": "localhost",
"port": 5432,
"credentials": {"username": "admin", "password": "secret"},
"options": {"ssl": True, "timeout": 30, "retry_attempts": 3},
},
"features": ["caching", "monitoring"],
"limits": {"max_connections": 100, "rate_limit": 1000},
}
instance1 = Model.model_validate(config1)
assert instance1.database["credentials"]["username"] == "admin"
# Test with token credentials
config2 = {
"version": "2.0.0",
"database": {
"host": "db.example.com",
"port": 3306,
"credentials": {"token": "bearer_token_123"},
},
"features": [],
"limits": {"max_connections": 50, "rate_limit": None},
}
instance2 = Model.model_validate(config2)
assert instance2.database["credentials"]["token"] == "bearer_token_123"
assert instance2.limits["rate_limit"] is None
def test_empty_items_schema_in_arrays(self) -> None:
"""Test handling of array fields with empty items schema."""
schema = {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The issue title",
},
"labels": {
"anyOf": [
{
"type": "array",
"items": {},
},
{"type": "null"},
],
"default": None,
"description": "Array of label names",
},
"links": {
"anyOf": [
{
"type": "array",
"items": {},
},
{"type": "null"},
],
"default": None,
"description": "Array of link objects",
},
"team": {
"type": "string",
"description": "The team name",
},
},
"required": ["title", "team"],
}
model = create_model_from_json_schema(schema, "EmptyItemsModel")
assert issubclass(model, BaseModel)
instance1 = model.model_validate(
{
"title": "Test Issue",
"team": "engineering",
"labels": None,
"links": None,
}
)
assert instance1.title == "Test Issue"
assert instance1.labels is None
instance2 = model.model_validate(
{
"title": "Test Issue 2",
"team": "engineering",
"labels": ["bug", "urgent", "backend"],
"links": [
{"url": "https://example.com", "title": "Link 1"},
{"url": "https://example.com/2", "title": "Link 2"},
],
}
)
assert instance2.labels == ["bug", "urgent", "backend"]
assert len(instance2.links) == 2
instance3 = model.model_validate(
{
"title": "Test Issue 3",
"team": "engineering",
"labels": ["string", 123, True, None, {"key": "value"}],
}
)
assert len(instance3.labels) == 5
dumped = instance2.model_dump()
assert dumped["labels"] == ["bug", "urgent", "backend"]
assert dumped["links"][0]["url"] == "https://example.com"