Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24192,7 +24192,7 @@ datamodel-code-generator detects the OpenAPI version from the `openapi` field:
| `deprecated` | 2019-09 | ⚠️ Partial | Recognized but not enforced |
| `examples` (array) | Draft 6 | ⚠️ Partial | Only first example used for Field default |
| Recursive `$ref` | Draft 4+ | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment |
| `propertyNames` | Draft 6 | ❌ Not supported | Property name validation ignored |
| `propertyNames` | Draft 6 | ✅ Supported | Dict key type constraints via pattern, enum, or $ref |
| `dependentRequired` | 2019-09 | ❌ Not supported | Dependent requirements ignored |
| `dependentSchemas` | 2019-09 | ❌ Not supported | Dependent schemas ignored |

Expand Down
2 changes: 1 addition & 1 deletion docs/supported_formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ datamodel-code-generator detects the OpenAPI version from the `openapi` field:
| `deprecated` | 2019-09 | ⚠️ Partial | Recognized but not enforced |
| `examples` (array) | Draft 6 | ⚠️ Partial | Only first example used for Field default |
| Recursive `$ref` | Draft 4+ | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment |
| `propertyNames` | Draft 6 | ❌ Not supported | Property name validation ignored |
| `propertyNames` | Draft 6 | ✅ Supported | Dict key type constraints via pattern, enum, or $ref |
| `dependentRequired` | 2019-09 | ❌ Not supported | Dependent requirements ignored |
| `dependentSchemas` | 2019-09 | ❌ Not supported | Dependent schemas ignored |

Expand Down
181 changes: 181 additions & 0 deletions scripts/build_schema_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""Schema documentation builder.

Generates feature tables from JsonSchemaFeatures and OpenAPISchemaFeatures
metadata for docs/supported_formats.md.

Usage:
python scripts/build_schema_docs.py # Generate/update docs
python scripts/build_schema_docs.py --check # Check if docs are up to date
"""

from __future__ import annotations

import argparse
import sys
from dataclasses import fields
from pathlib import Path

# Add src to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from datamodel_code_generator.parser.schema_version import (
FeatureMetadata,
JsonSchemaFeatures,
OpenAPISchemaFeatures,
)

DOCS_PATH = Path(__file__).parent.parent / "docs" / "supported_formats.md"

# Status emoji mapping
STATUS_EMOJI = {
"supported": "✅",
"partial": "⚠️",
"not_supported": "❌",
}


def get_feature_metadata(cls: type) -> list[tuple[str, FeatureMetadata]]:
"""Extract feature metadata from a dataclass.

Args:
cls: JsonSchemaFeatures or OpenAPISchemaFeatures class.

Returns:
List of (field_name, metadata) tuples.
"""
result = []
for f in fields(cls):
if f.metadata:
meta = FeatureMetadata(
introduced=f.metadata.get("introduced", ""),
doc_name=f.metadata.get("doc_name", f.name),
description=f.metadata.get("description", ""),
status=f.metadata.get("status", "supported"),
)
result.append((f.name, meta))
return result


def generate_feature_table(
features: list[tuple[str, FeatureMetadata]],
*,
include_status: bool = True,
) -> str:
"""Generate a Markdown table from feature metadata.

Args:
features: List of (field_name, metadata) tuples.
include_status: Whether to include the Status column.

Returns:
Markdown table string.
"""
if include_status:
lines = ["| Feature | Introduced | Status | Description |"]
lines.append("|---------|------------|--------|-------------|")
for _name, meta in features:
emoji = STATUS_EMOJI.get(meta["status"], "")
status_text = f"{emoji} {meta['status'].replace('_', ' ').title()}"
lines.append(f"| `{meta['doc_name']}` | {meta['introduced']} | {status_text} | {meta['description']} |")
else:
lines = ["| Feature | Introduced | Description |"]
lines.append("|---------|------------|-------------|")
for _name, meta in features:
lines.append(f"| `{meta['doc_name']}` | {meta['introduced']} | {meta['description']} |")

return "\n".join(lines)


def print_features_summary() -> None:
"""Print a summary of all features with their metadata."""
print("=" * 60)
print("JSON Schema Features")
print("=" * 60)
for name, meta in get_feature_metadata(JsonSchemaFeatures):
print(f" {name}:")
print(f" doc_name: {meta['doc_name']}")
print(f" introduced: {meta['introduced']}")
print(f" status: {meta['status']}")
print(f" description: {meta['description']}")
print()

print("=" * 60)
print("OpenAPI Schema Features (additional)")
print("=" * 60)
# Get only OpenAPI-specific fields (not inherited from JsonSchemaFeatures)
json_field_names = {f.name for f in fields(JsonSchemaFeatures)}
for name, meta in get_feature_metadata(OpenAPISchemaFeatures):
if name not in json_field_names:
print(f" {name}:")
print(f" doc_name: {meta['doc_name']}")
print(f" introduced: {meta['introduced']}")
print(f" status: {meta['status']}")
print(f" description: {meta['description']}")
print()


def generate_supported_features_table() -> str:
"""Generate the supported features table for documentation."""
lines = [
"<!-- BEGIN AUTO-GENERATED SUPPORTED FEATURES -->",
"",
"### Supported Features (from code)",
"",
"The following features are tracked in the codebase with their implementation status:",
"",
"#### JSON Schema Features",
"",
generate_feature_table(get_feature_metadata(JsonSchemaFeatures)),
"",
"#### OpenAPI-Specific Features",
"",
]

# Get only OpenAPI-specific fields
json_field_names = {f.name for f in fields(JsonSchemaFeatures)}
openapi_features = [
(name, meta) for name, meta in get_feature_metadata(OpenAPISchemaFeatures) if name not in json_field_names
]
lines.extend((generate_feature_table(openapi_features), "", "<!-- END AUTO-GENERATED SUPPORTED FEATURES -->"))

return "\n".join(lines)


def main() -> int:
"""Parse arguments and build documentation."""
parser = argparse.ArgumentParser(description="Build schema documentation from code metadata")
parser.add_argument(
"--check",
action="store_true",
help="Check if docs would change without modifying files",
)
parser.add_argument(
"--summary",
action="store_true",
help="Print a summary of all features and their metadata",
)
args = parser.parse_args()

if args.summary:
print_features_summary()
return 0

print("Schema Documentation Builder")
print("-" * 40)

# For now, just print the generated table
print("\nGenerated Supported Features Table:")
print()
print(generate_supported_features_table())

if args.check:
print("\n[Check mode] No files modified.")
else:
print("\n[Info] This script currently outputs to stdout.")
print(" Future versions will update docs/supported_formats.md directly.")

return 0


if __name__ == "__main__":
sys.exit(main())
140 changes: 116 additions & 24 deletions src/datamodel_code_generator/parser/schema_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,117 @@

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar

from typing_extensions import TypedDict

from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion

if TYPE_CHECKING:
from datamodel_code_generator.types import Types


class FeatureMetadata(TypedDict):
"""Metadata for schema feature documentation.

This metadata is used by scripts/build_schema_docs.py to generate
the feature compatibility matrix in docs/supported_formats.md.
"""

introduced: str
"""Version when feature was introduced (e.g., "Draft 6", "2020-12", "OAS 3.0")."""
doc_name: str
"""Display name for documentation (e.g., "prefixItems", "Null in type array")."""
description: str
"""User-facing description of the feature."""
status: Literal["supported", "partial", "not_supported"]
"""Implementation status: supported, partial, or not_supported."""


@dataclass(frozen=True)
class JsonSchemaFeatures:
"""Feature flags for JSON Schema versions.

This is the base class for schema features. OpenAPISchemaFeatures
extends this to add OpenAPI-specific features.

Attributes:
null_in_type_array: Draft 2020-12 allows null in type arrays.
defs_not_definitions: Draft 2019-09+ uses $defs instead of definitions.
prefix_items: Draft 2020-12 uses prefixItems instead of items array.
boolean_schemas: Draft 6+ allows boolean values as schemas.
id_field: The field name for schema ID ("id" for Draft 4, "$id" for Draft 6+).
definitions_key: The key for definitions ("definitions" or "$defs").
exclusive_as_number: Draft 6+ uses numeric exclusiveMin/Max (Draft 4 uses boolean).
read_only_write_only: Draft 7+ supports readOnly/writeOnly keywords.
Each field includes metadata for documentation generation.
Use `dataclasses.fields(JsonSchemaFeatures)` to access metadata.
"""

null_in_type_array: bool
defs_not_definitions: bool
prefix_items: bool
boolean_schemas: bool
id_field: str
definitions_key: str
exclusive_as_number: bool
read_only_write_only: bool
null_in_type_array: bool = field(
default=False,
metadata=FeatureMetadata(
introduced="2020-12",
doc_name="Null in type array",
description="Allows `type: ['string', 'null']` syntax for nullable types",
status="supported",
),
)
defs_not_definitions: bool = field(
default=False,
metadata=FeatureMetadata(
introduced="2019-09",
doc_name="$defs",
description="Uses `$defs` instead of `definitions` for schema definitions",
status="supported",
),
)
prefix_items: bool = field(
default=False,
metadata=FeatureMetadata(
introduced="2020-12",
doc_name="prefixItems",
description="Tuple validation using `prefixItems` keyword",
status="supported",
),
)
boolean_schemas: bool = field(
default=False,
metadata=FeatureMetadata(
introduced="Draft 6",
doc_name="Boolean schemas",
description="Allows `true` and `false` as valid schemas",
status="supported",
),
)
id_field: str = field(
default="$id",
metadata=FeatureMetadata(
introduced="Draft 6",
doc_name="$id",
description="Schema identifier field (`id` in Draft 4, `$id` in Draft 6+)",
status="supported",
),
)
definitions_key: str = field(
default="$defs",
metadata=FeatureMetadata(
introduced="Draft 4",
doc_name="definitions/$defs",
description="Key for reusable schema definitions",
status="supported",
),
)
exclusive_as_number: bool = field(
default=False,
metadata=FeatureMetadata(
introduced="Draft 6",
doc_name="exclusiveMinimum/Maximum as number",
description="Numeric `exclusiveMinimum`/`exclusiveMaximum` (boolean in Draft 4)",
status="supported",
),
)
read_only_write_only: bool = field(
default=False,
metadata=FeatureMetadata(
introduced="Draft 7",
doc_name="readOnly/writeOnly",
description="Field visibility hints for read-only and write-only properties",
status="supported",
),
)

@classmethod
def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures:
Expand Down Expand Up @@ -110,13 +186,28 @@ class OpenAPISchemaFeatures(JsonSchemaFeatures):

Extends JsonSchemaFeatures with OpenAPI-specific features.

Attributes:
nullable_keyword: OpenAPI 3.0 uses nullable: true (deprecated in 3.1).
discriminator_support: All OpenAPI versions support discriminator.
Each field includes metadata for documentation generation.
Use `dataclasses.fields(OpenAPISchemaFeatures)` to access metadata.
"""

nullable_keyword: bool
discriminator_support: bool
nullable_keyword: bool = field(
default=False,
metadata=FeatureMetadata(
introduced="OAS 3.0",
doc_name="nullable",
description="Uses `nullable: true` for nullable types (deprecated in 3.1)",
status="supported",
),
)
discriminator_support: bool = field(
default=True,
metadata=FeatureMetadata(
introduced="OAS 3.0",
doc_name="discriminator",
description="Polymorphism support via `discriminator` keyword",
status="supported",
),
)

@classmethod
def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures:
Expand Down Expand Up @@ -310,6 +401,7 @@ def get_data_formats(*, is_openapi: bool = False) -> DataFormatMapping:

__all__ = [
"DataFormatMapping",
"FeatureMetadata",
"JsonSchemaFeatures",
"OpenAPISchemaFeatures",
"SchemaFeaturesT",
Expand Down
Loading