import datetime import ipaddress from collections.abc import Sequence from dataclasses import MISSING, dataclass, field from enum import Enum from typing import Any, Dict, List, Optional, Union from typing_extensions import TYPE_CHECKING, Self, TypeAlias from mashumaro.config import BaseConfig from mashumaro.core.meta.helpers import iter_all_subclasses from mashumaro.helper import pass_through from mashumaro.jsonschema.dialects import DRAFT_2020_12, JSONSchemaDialect if TYPE_CHECKING: # pragma: no cover from mashumaro.jsonschema.plugins import BasePlugin else: BasePlugin = Any try: from mashumaro.mixins.orjson import ( DataClassORJSONMixin as DataClassJSONMixin, ) except ImportError: # pragma: no cover from mashumaro.mixins.json import DataClassJSONMixin # type: ignore # https://github.com/python/mypy/issues/3186 Number: TypeAlias = Union[int, float] Null = object() class JSONSchemaInstanceType(Enum): NULL = "null" BOOLEAN = "boolean" OBJECT = "object" ARRAY = "array" NUMBER = "number" STRING = "string" INTEGER = "integer" class JSONSchemaInstanceFormat(Enum): pass class JSONSchemaStringFormat(JSONSchemaInstanceFormat): DATETIME = "date-time" DATE = "date" TIME = "time" DURATION = "duration" EMAIL = "email" IDN_EMAIL = "idn-email" HOSTNAME = "hostname" IDN_HOSTNAME = "idn-hostname" IPV4ADDRESS = "ipv4" IPV6ADDRESS = "ipv6" URI = "uri" URI_REFERENCE = "uri-reference" IRI = "iri" IRI_REFERENCE = "iri-reference" UUID = "uuid" URI_TEMPLATE = "uri-template" JSON_POINTER = "json-pointer" RELATIVE_JSON_POINTER = "relative-json-pointer" REGEX = "regex" class JSONSchemaInstanceFormatExtension(JSONSchemaInstanceFormat): TIMEDELTA = "time-delta" TIME_ZONE = "time-zone" IPV4NETWORK = "ipv4network" IPV6NETWORK = "ipv6network" IPV4INTERFACE = "ipv4interface" IPV6INTERFACE = "ipv6interface" DECIMAL = "decimal" FRACTION = "fraction" BASE64 = "base64" PATH = "path" DATETIME_FORMATS = { datetime.datetime: JSONSchemaStringFormat.DATETIME, datetime.date: JSONSchemaStringFormat.DATE, datetime.time: JSONSchemaStringFormat.TIME, } IPADDRESS_FORMATS = { ipaddress.IPv4Address: JSONSchemaStringFormat.IPV4ADDRESS, ipaddress.IPv6Address: JSONSchemaStringFormat.IPV6ADDRESS, ipaddress.IPv4Network: JSONSchemaInstanceFormatExtension.IPV4NETWORK, ipaddress.IPv6Network: JSONSchemaInstanceFormatExtension.IPV6NETWORK, ipaddress.IPv4Interface: JSONSchemaInstanceFormatExtension.IPV4INTERFACE, ipaddress.IPv6Interface: JSONSchemaInstanceFormatExtension.IPV6INTERFACE, } def _deserialize_json_schema_instance_format( value: Any, ) -> JSONSchemaInstanceFormat: for cls in iter_all_subclasses(JSONSchemaInstanceFormat): try: return cls(value) except (ValueError, TypeError): pass raise ValueError(value) @dataclass(unsafe_hash=True) class JSONSchema(DataClassJSONMixin): # Common keywords schema: Optional[str] = None type: Optional[JSONSchemaInstanceType] = None enum: Optional[list[Any]] = None const: Optional[Any] = field(default_factory=lambda: MISSING) format: Optional[JSONSchemaInstanceFormat] = None title: Optional[str] = None description: Optional[str] = None anyOf: Optional[List["JSONSchema"]] = None reference: Optional[str] = None definitions: Optional[Dict[str, "JSONSchema"]] = None default: Optional[Any] = field(default_factory=lambda: MISSING) deprecated: Optional[bool] = None examples: Optional[list[Any]] = None # Keywords for Objects properties: Optional[Dict[str, "JSONSchema"]] = None patternProperties: Optional[Dict[str, "JSONSchema"]] = None additionalProperties: Union["JSONSchema", bool, None] = None propertyNames: Optional["JSONSchema"] = None # Keywords for Arrays prefixItems: Optional[List["JSONSchema"]] = None items: Optional["JSONSchema"] = None contains: Optional["JSONSchema"] = None # Validation keywords for numeric instances multipleOf: Optional[Number] = None maximum: Optional[Number] = None exclusiveMaximum: Optional[Number] = None minimum: Optional[Number] = None exclusiveMinimum: Optional[Number] = None # Validation keywords for Strings maxLength: Optional[int] = None minLength: Optional[int] = None pattern: Optional[str] = None # Validation keywords for Arrays maxItems: Optional[int] = None minItems: Optional[int] = None uniqueItems: Optional[bool] = None maxContains: Optional[int] = None minContains: Optional[int] = None # Validation keywords for Objects maxProperties: Optional[int] = None minProperties: Optional[int] = None required: Optional[list[str]] = None dependentRequired: Optional[dict[str, set[str]]] = None class Config(BaseConfig): omit_none = True serialize_by_alias = True aliases = { "schema": "$schema", "reference": "$ref", "definitions": "$defs", } serialization_strategy = { int: pass_through, float: pass_through, Null: pass_through, JSONSchemaInstanceFormat: { "deserialize": _deserialize_json_schema_instance_format, }, } def __pre_serialize__(self) -> Self: if self.const is None: self.const = Null if self.default is None: self.default = Null return self def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: const = d.get("const") if const is MISSING: d.pop("const") elif const is Null: d["const"] = None default = d.get("default") if default is MISSING: d.pop("default") elif default is Null: d["default"] = None return d @dataclass class JSONObjectSchema(JSONSchema): type: Optional[JSONSchemaInstanceType] = JSONSchemaInstanceType.OBJECT @dataclass class JSONArraySchema(JSONSchema): type: Optional[JSONSchemaInstanceType] = JSONSchemaInstanceType.ARRAY @dataclass class Context: dialect: JSONSchemaDialect = DRAFT_2020_12 definitions: dict[str, JSONSchema] = field(default_factory=dict) all_refs: Optional[bool] = None ref_prefix: Optional[str] = None plugins: Sequence[BasePlugin] = ()