diff --git a/docs/cli-reference/field-customization.md b/docs/cli-reference/field-customization.md index f4a1f2aab..e23451edc 100644 --- a/docs/cli-reference/field-customization.md +++ b/docs/cli-reference/field-customization.md @@ -1339,29 +1339,58 @@ The `--field-extra-keys` flag configures the code generation behavior. ??? example "Output" - ```python - # generated by datamodel-codegen: - # filename: extras.json - # timestamp: 2019-07-26T00:00:00+00:00 - - from __future__ import annotations - - from typing import Optional - - from pydantic import BaseModel, Field - - - class Extras(BaseModel): - name: Optional[str] = Field( - None, - description='normal key', - example='example', - invalid_key_1='abc', - key2=456, - repr=True, - ) - age: Optional[int] = Field(None, example=12, examples=[13, 20]) - ``` + === "Pydantic v1" + + ```python + # generated by datamodel-codegen: + # filename: extras.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Optional + + from pydantic import BaseModel, Field + + + class Extras(BaseModel): + name: Optional[str] = Field( + None, + description='normal key', + example='example', + invalid_key_1='abc', + key2=456, + repr=True, + ) + age: Optional[int] = Field(None, example=12, examples=[13, 20]) + ``` + + === "Pydantic v2" + + ```python + # generated by datamodel-codegen: + # filename: extras.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Optional + + from pydantic import BaseModel, Field + + + class Extras(BaseModel): + name: Optional[str] = Field( + None, + description='normal key', + examples=['example'], + json_schema_extra={'key2': 456, 'invalid-key-1': 'abc'}, + repr=True, + ) + age: Optional[int] = Field( + None, examples=[13, 20], json_schema_extra={'example': 12} + ) + ``` --- @@ -1422,37 +1451,77 @@ in Field(). This is useful for custom schema extensions and vendor-specific meta ??? example "Output" - ```python - # generated by datamodel-codegen: - # filename: extras.json - # timestamp: 2019-07-26T00:00:00+00:00 - - from __future__ import annotations - - from typing import Optional - - from pydantic import BaseModel, Field - - - class Extras(BaseModel): - name: Optional[str] = Field( - None, - description='normal key', - example='example', - field_comment='comment', - field_exclude=123, - field_invalid_key_2='efg', - invalid_key_1='abc', - key1=123, - key2=456, - readOnly=True, - register_='hij', - repr=True, - schema_='klm', - x_abc=True, - ) - age: Optional[int] = Field(None, example=12, examples=[13, 20], writeOnly=True) - ``` + === "Pydantic v1" + + ```python + # generated by datamodel-codegen: + # filename: extras.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Optional + + from pydantic import BaseModel, Field + + + class Extras(BaseModel): + name: Optional[str] = Field( + None, + description='normal key', + example='example', + field_comment='comment', + field_exclude=123, + field_invalid_key_2='efg', + invalid_key_1='abc', + key1=123, + key2=456, + readOnly=True, + register_='hij', + repr=True, + schema_='klm', + x_abc=True, + ) + age: Optional[int] = Field(None, example=12, examples=[13, 20], writeOnly=True) + ``` + + === "Pydantic v2" + + ```python + # generated by datamodel-codegen: + # filename: extras.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Optional + + from pydantic import BaseModel, Field + + + class Extras(BaseModel): + name: Optional[str] = Field( + None, + description='normal key', + examples=['example'], + json_schema_extra={ + 'key1': 123, + 'key2': 456, + '$exclude': 123, + 'invalid-key-1': 'abc', + '-invalid+key_2': 'efg', + '$comment': 'comment', + 'register': 'hij', + 'schema': 'klm', + 'x-abc': True, + 'readOnly': True, + }, + repr=True, + ) + age: Optional[int] = Field( + None, examples=[13, 20], json_schema_extra={'example': 12, 'writeOnly': True} + ) + ``` --- diff --git a/docs/cli-reference/model-customization.md b/docs/cli-reference/model-customization.md index 605ff3ba2..11ac04912 100644 --- a/docs/cli-reference/model-customization.md +++ b/docs/cli-reference/model-customization.md @@ -15,7 +15,7 @@ | [`--frozen-dataclasses`](#frozen-dataclasses) | Generate frozen dataclasses with optional keyword-only field... | | [`--keep-model-order`](#keep-model-order) | Keep model definition order as specified in schema. | | [`--keyword-only`](#keyword-only) | Generate dataclasses with keyword-only fields (Python 3.10+)... | -| [`--output-model-type`](#output-model-type) | Specify the output model type (pydantic, dataclass, typeddic... | +| [`--output-model-type`](#output-model-type) | Select the output model type (Pydantic v1/v2, dataclasses, T... | | [`--parent-scoped-naming`](#parent-scoped-naming) | Namespace models by their parent scope to avoid naming confl... | | [`--reuse-model`](#reuse-model) | Reuse identical model definitions instead of generating dupl... | | [`--reuse-scope`](#reuse-scope) | Scope for model reuse detection (root or tree). | @@ -26,7 +26,7 @@ | [`--union-mode`](#union-mode) | Union mode for combining anyOf/oneOf schemas (smart or left_... | | [`--use-default`](#use-default) | Use default values from schema in generated models. | | [`--use-default-kwarg`](#use-default-kwarg) | Use default= keyword argument instead of positional argument... | -| [`--use-frozen-field`](#use-frozen-field) | Generate frozen (immutable) field definitions. | +| [`--use-frozen-field`](#use-frozen-field) | Generate frozen (immutable) field definitions for readOnly p... | | [`--use-one-literal-as-default`](#use-one-literal-as-default) | Use single literal value as default when enum has only one o... | | [`--use-serialize-as-any`](#use-serialize-as-any) | Wrap fields with subtypes in Pydantic's SerializeAsAny. | | [`--use-subclass-enum`](#use-subclass-enum) | Generate typed Enum subclasses for enums with specific field... | @@ -2947,9 +2947,12 @@ argument errors. ## `--output-model-type` {#output-model-type} -Specify the output model type (pydantic, dataclass, typeddict, msgspec). +Select the output model type (Pydantic v1/v2, dataclasses, TypedDict, msgspec). -The `--output-model-type` flag configures the code generation behavior. +The `--output-model-type` flag specifies which Python data model framework to use +for the generated code. Supported values include `pydantic.BaseModel`, +`pydantic_v2.BaseModel`, `dataclasses.dataclass`, `typing.TypedDict`, and +`msgspec.Struct`. !!! tip "Usage" @@ -2992,25 +2995,49 @@ The `--output-model-type` flag configures the code generation behavior. ??? example "Output" - ```python - # generated by datamodel-codegen: - # filename: null_and_array.json - # timestamp: 2019-07-26T00:00:00+00:00 - - from __future__ import annotations - - from typing import Any, List, Optional - - from pydantic import BaseModel - - - class MyObjItem(BaseModel): - items: Optional[List[Any]] - - - class Model(BaseModel): - my_obj: List[MyObjItem] - ``` + === "Pydantic v1" + + ```python + # generated by datamodel-codegen: + # filename: null_and_array.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Any, List, Optional + + from pydantic import BaseModel + + + class MyObjItem(BaseModel): + items: Optional[List[Any]] + + + class Model(BaseModel): + my_obj: List[MyObjItem] + ``` + + === "Pydantic v2" + + ```python + # generated by datamodel-codegen: + # filename: null_and_array.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Any, List, Optional + + from pydantic import BaseModel + + + class MyObjItem(BaseModel): + items: Optional[List[Any]] = None + + + class Model(BaseModel): + my_obj: List[MyObjItem] + ``` --- @@ -4296,14 +4323,17 @@ default values. ## `--use-frozen-field` {#use-frozen-field} -Generate frozen (immutable) field definitions. +Generate frozen (immutable) field definitions for readOnly properties. -The `--use-frozen-field` flag configures the code generation behavior. +The `--use-frozen-field` flag generates frozen field definitions: +- Pydantic v1: `Field(allow_mutation=False)` +- Pydantic v2: `Field(frozen=True)` +- Dataclasses: silently ignored (no frozen fields generated) !!! tip "Usage" ```bash - datamodel-codegen --input schema.json --use-frozen-field --output-model-type pydantic_v2.BaseModel # (1)! + datamodel-codegen --input schema.json --use-frozen-field # (1)! ``` 1. :material-arrow-left: `--use-frozen-field` - the option documented here @@ -4341,24 +4371,72 @@ The `--use-frozen-field` flag configures the code generation behavior. ??? example "Output" - ```python - # generated by datamodel-codegen: - # filename: use_frozen_field.json - # timestamp: 2019-07-26T00:00:00+00:00 - - from __future__ import annotations - - from typing import Optional - - from pydantic import AwareDatetime, BaseModel, Field - - - class User(BaseModel): - id: int = Field(..., description='Server-generated ID', frozen=True) - name: str - password: str = Field(..., description='User password') - created_at: Optional[AwareDatetime] = Field(None, frozen=True) - ``` + === "Pydantic v1" + + ```python + # generated by datamodel-codegen: + # filename: use_frozen_field.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from datetime import datetime + from typing import Optional + + from pydantic import BaseModel, Field + + + class User(BaseModel): + class Config: + validate_assignment = True + + id: int = Field(..., allow_mutation=False, description='Server-generated ID') + name: str + password: str = Field(..., description='User password') + created_at: Optional[datetime] = Field(None, allow_mutation=False) + ``` + + === "Pydantic v2" + + ```python + # generated by datamodel-codegen: + # filename: use_frozen_field.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Optional + + from pydantic import AwareDatetime, BaseModel, Field + + + class User(BaseModel): + id: int = Field(..., description='Server-generated ID', frozen=True) + name: str + password: str = Field(..., description='User password') + created_at: Optional[AwareDatetime] = Field(None, frozen=True) + ``` + + === "dataclass" + + ```python + # generated by datamodel-codegen: + # filename: use_frozen_field.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from dataclasses import dataclass + from typing import Optional + + + @dataclass + class User: + id: int + name: str + password: str + created_at: Optional[str] = None + ``` --- diff --git a/docs/cli-reference/quick-reference.md b/docs/cli-reference/quick-reference.md index f827d200b..3e287d285 100644 --- a/docs/cli-reference/quick-reference.md +++ b/docs/cli-reference/quick-reference.md @@ -26,9 +26,9 @@ datamodel-codegen [OPTIONS] | Option | Description | |--------|-------------| -| [`--allof-merge-mode`](typing-customization.md#allof-merge-mode) | Merge all properties from parent schemas in allOf. | +| [`--allof-merge-mode`](typing-customization.md#allof-merge-mode) | Merge constraints from root model references in allOf schemas. | | [`--disable-future-imports`](typing-customization.md#disable-future-imports) | Prevent automatic addition of __future__ imports in generated code. | -| [`--enum-field-as-literal`](typing-customization.md#enum-field-as-literal) | Generate Literal types instead of Enums for fields with enumerated values. | +| [`--enum-field-as-literal`](typing-customization.md#enum-field-as-literal) | Convert all enum fields to Literal types instead of Enum classes. | | [`--no-use-specialized-enum`](typing-customization.md#no-use-specialized-enum) | Disable specialized Enum classes for Python 3.11+ code generation. | | [`--output-datetime-class`](typing-customization.md#output-datetime-class) | Specify datetime class type for date-time schema fields. | | [`--strict-types`](typing-customization.md#strict-types) | Enable strict type validation for specified Python types. | @@ -83,7 +83,7 @@ datamodel-codegen [OPTIONS] | [`--frozen-dataclasses`](model-customization.md#frozen-dataclasses) | Generate frozen dataclasses with optional keyword-only fields. | | [`--keep-model-order`](model-customization.md#keep-model-order) | Keep model definition order as specified in schema. | | [`--keyword-only`](model-customization.md#keyword-only) | Generate dataclasses with keyword-only fields (Python 3.10+). | -| [`--output-model-type`](model-customization.md#output-model-type) | Specify the output model type (pydantic, dataclass, typeddict, msgspec). | +| [`--output-model-type`](model-customization.md#output-model-type) | Select the output model type (Pydantic v1/v2, dataclasses, TypedDict, msgspec). | | [`--parent-scoped-naming`](model-customization.md#parent-scoped-naming) | Namespace models by their parent scope to avoid naming conflicts. | | [`--reuse-model`](model-customization.md#reuse-model) | Reuse identical model definitions instead of generating duplicates. | | [`--reuse-scope`](model-customization.md#reuse-scope) | Scope for model reuse detection (root or tree). | @@ -94,7 +94,7 @@ datamodel-codegen [OPTIONS] | [`--union-mode`](model-customization.md#union-mode) | Union mode for combining anyOf/oneOf schemas (smart or left_to_right). | | [`--use-default`](model-customization.md#use-default) | Use default values from schema in generated models. | | [`--use-default-kwarg`](model-customization.md#use-default-kwarg) | Use default= keyword argument instead of positional argument for fields with def... | -| [`--use-frozen-field`](model-customization.md#use-frozen-field) | Generate frozen (immutable) field definitions. | +| [`--use-frozen-field`](model-customization.md#use-frozen-field) | Generate frozen (immutable) field definitions for readOnly properties. | | [`--use-one-literal-as-default`](model-customization.md#use-one-literal-as-default) | Use single literal value as default when enum has only one option. | | [`--use-serialize-as-any`](model-customization.md#use-serialize-as-any) | Wrap fields with subtypes in Pydantic's SerializeAsAny. | | [`--use-subclass-enum`](model-customization.md#use-subclass-enum) | Generate typed Enum subclasses for enums with specific field types. | @@ -155,7 +155,7 @@ All options sorted alphabetically: - [`--aliases`](field-customization.md#aliases) - Apply custom field and class name aliases from JSON file. - [`--all-exports-collision-strategy`](general-options.md#all-exports-collision-strategy) - Handle name collisions when exporting recursive module hiera... - [`--all-exports-scope`](general-options.md#all-exports-scope) - Generate __all__ exports for child modules in __init__.py fi... -- [`--allof-merge-mode`](typing-customization.md#allof-merge-mode) - Merge all properties from parent schemas in allOf. +- [`--allof-merge-mode`](typing-customization.md#allof-merge-mode) - Merge constraints from root model references in allOf schema... - [`--allow-extra-fields`](model-customization.md#allow-extra-fields) - Allow extra fields in generated Pydantic models (extra='allo... - [`--allow-population-by-field-name`](model-customization.md#allow-population-by-field-name) - Allow Pydantic model population by field name (not just alia... - [`--base-class`](model-customization.md#base-class) - Specify a custom base class for generated models. @@ -177,7 +177,7 @@ All options sorted alphabetically: - [`--enable-faux-immutability`](model-customization.md#enable-faux-immutability) - Enable faux immutability in Pydantic v1 models (allow_mutati... - [`--enable-version-header`](template-customization.md#enable-version-header) - Include tool version information in file header. - [`--encoding`](base-options.md#encoding) - Specify character encoding for input and output files. -- [`--enum-field-as-literal`](typing-customization.md#enum-field-as-literal) - Generate Literal types instead of Enums for fields with enum... +- [`--enum-field-as-literal`](typing-customization.md#enum-field-as-literal) - Convert all enum fields to Literal types instead of Enum cla... - [`--extra-fields`](field-customization.md#extra-fields) - Configure how generated models handle extra fields not defin... - [`--extra-template-data`](template-customization.md#extra-template-data) - Pass custom template variables from JSON file for code gener... - [`--field-constraints`](field-customization.md#field-constraints) - Generate Field() with validation constraints from schema. @@ -204,7 +204,7 @@ All options sorted alphabetically: - [`--original-field-name-delimiter`](field-customization.md#original-field-name-delimiter) - Specify delimiter for original field names when using snake-... - [`--output`](base-options.md#output) - Specify the destination path for generated Python code. - [`--output-datetime-class`](typing-customization.md#output-datetime-class) - Specify datetime class type for date-time schema fields. -- [`--output-model-type`](model-customization.md#output-model-type) - Specify the output model type (pydantic, dataclass, typeddic... +- [`--output-model-type`](model-customization.md#output-model-type) - Select the output model type (Pydantic v1/v2, dataclasses, T... - [`--parent-scoped-naming`](model-customization.md#parent-scoped-naming) - Namespace models by their parent scope to avoid naming confl... - [`--read-only-write-only-model-type`](openapi-only-options.md#read-only-write-only-model-type) - Generate separate request and response models for readOnly/w... - [`--remove-special-field-name-prefix`](field-customization.md#remove-special-field-name-prefix) - Remove the special prefix from field names. @@ -232,7 +232,7 @@ All options sorted alphabetically: - [`--use-enum-values-in-discriminator`](field-customization.md#use-enum-values-in-discriminator) - Use enum values in discriminator mappings for union types. - [`--use-exact-imports`](template-customization.md#use-exact-imports) - Import exact types instead of modules. - [`--use-field-description`](field-customization.md#use-field-description) - Include schema descriptions as Field docstrings. -- [`--use-frozen-field`](model-customization.md#use-frozen-field) - Generate frozen (immutable) field definitions. +- [`--use-frozen-field`](model-customization.md#use-frozen-field) - Generate frozen (immutable) field definitions for readOnly p... - [`--use-generic-container-types`](typing-customization.md#use-generic-container-types) - Use typing.Dict/List instead of dict/list for container type... - [`--use-inline-field-description`](field-customization.md#use-inline-field-description) - Add field descriptions as inline comments. - [`--use-non-positive-negative-number-constrained-types`](typing-customization.md#use-non-positive-negative-number-constrained-types) - Use NonPositive/NonNegative types for number constraints. diff --git a/docs/cli-reference/typing-customization.md b/docs/cli-reference/typing-customization.md index ba9b421cd..783ffbb93 100644 --- a/docs/cli-reference/typing-customization.md +++ b/docs/cli-reference/typing-customization.md @@ -4,9 +4,9 @@ | Option | Description | |--------|-------------| -| [`--allof-merge-mode`](#allof-merge-mode) | Merge all properties from parent schemas in allOf. | +| [`--allof-merge-mode`](#allof-merge-mode) | Merge constraints from root model references in allOf schema... | | [`--disable-future-imports`](#disable-future-imports) | Prevent automatic addition of __future__ imports in generate... | -| [`--enum-field-as-literal`](#enum-field-as-literal) | Generate Literal types instead of Enums for fields with enum... | +| [`--enum-field-as-literal`](#enum-field-as-literal) | Convert all enum fields to Literal types instead of Enum cla... | | [`--no-use-specialized-enum`](#no-use-specialized-enum) | Disable specialized Enum classes for Python 3.11+ code gener... | | [`--output-datetime-class`](#output-datetime-class) | Specify datetime class type for date-time schema fields. | | [`--strict-types`](#strict-types) | Enable strict type validation for specified Python types. | @@ -25,75 +25,571 @@ ## `--allof-merge-mode` {#allof-merge-mode} -Merge all properties from parent schemas in allOf. +Merge constraints from root model references in allOf schemas. -The `--allof-merge-mode` flag controls how parent schema properties are merged -in allOf compositions. With `all` mode, constraints plus annotations (default, -examples) are merged from parent properties. This ensures child schemas inherit -all metadata from parents. +The `--allof-merge-mode constraints` merges only constraint properties +(minLength, maximum, etc.) from parent schemas referenced in allOf. +This ensures child schemas inherit validation constraints while keeping +other properties separate. !!! tip "Usage" ```bash - datamodel-codegen --input schema.json --allof-merge-mode all # (1)! + datamodel-codegen --input schema.json --allof-merge-mode constraints # (1)! ``` 1. :material-arrow-left: `--allof-merge-mode` - the option documented here ??? example "Input Schema" - ```yaml - openapi: "3.0.0" - info: - title: Test materialize allOf defaults - version: "1.0.0" - components: - schemas: - Parent: - type: object - properties: - name: - type: string - default: "parent_default" - minLength: 1 - count: - type: integer - default: 10 - minimum: 0 - Child: - allOf: - - $ref: "#/components/schemas/Parent" - - type: object - properties: - name: - maxLength: 100 - count: - maximum: 1000 + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "StringDatatype": { + "description": "A base string type.", + "type": "string", + "pattern": "^\\S(.*\\S)?$" + }, + "ConstrainedStringDatatype": { + "description": "A constrained string.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype" }, + { "type": "string", "minLength": 1, "pattern": "^[A-Z].*" } + ] + }, + "IntegerDatatype": { + "description": "A whole number.", + "type": "integer" + }, + "NonNegativeIntegerDatatype": { + "description": "Non-negative integer.", + "allOf": [ + { "$ref": "#/definitions/IntegerDatatype" }, + { "minimum": 0 } + ] + }, + "BoundedIntegerDatatype": { + "description": "Integer between 0 and 100.", + "allOf": [ + { "$ref": "#/definitions/IntegerDatatype" }, + { "minimum": 0, "maximum": 100 } + ] + }, + "EmailDatatype": { + "description": "Email with format.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype" }, + { "format": "email" } + ] + }, + "FormattedStringDatatype": { + "description": "A string with email format.", + "type": "string", + "format": "email" + }, + "ObjectBase": { + "type": "object", + "properties": { + "id": { "type": "integer" } + } + }, + "ObjectWithAllOf": { + "description": "Object inheritance - not a root model.", + "allOf": [ + { "$ref": "#/definitions/ObjectBase" }, + { "type": "object", "properties": { "name": { "type": "string" } } } + ] + }, + "MultiRefAllOf": { + "description": "Multiple refs - not handled by new code.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype" }, + { "$ref": "#/definitions/IntegerDatatype" } + ] + }, + "NoConstraintAllOf": { + "description": "No constraints added.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype" } + ] + }, + "IncompatibleTypeAllOf": { + "description": "Incompatible types.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype" }, + { "type": "boolean" } + ] + }, + "ConstraintWithProperties": { + "description": "Constraint item has properties.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype" }, + { "properties": { "extra": { "type": "string" } } } + ] + }, + "ConstraintWithItems": { + "description": "Constraint item has items.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype" }, + { "items": { "type": "string" } } + ] + }, + "NumberIntegerCompatible": { + "description": "Number and integer are compatible.", + "allOf": [ + { "$ref": "#/definitions/IntegerDatatype" }, + { "type": "number", "minimum": 0 } + ] + }, + "RefWithSchemaKeywords": { + "description": "Ref with additional schema keywords.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype", "minLength": 5 }, + { "maxLength": 100 } + ] + }, + "ArrayDatatype": { + "type": "array", + "items": { "type": "string" } + }, + "RefToArrayAllOf": { + "description": "Ref to array - not a root model.", + "allOf": [ + { "$ref": "#/definitions/ArrayDatatype" }, + { "minItems": 1 } + ] + }, + "ObjectNoPropsDatatype": { + "type": "object" + }, + "RefToObjectNoPropsAllOf": { + "description": "Ref to object without properties - not a root model.", + "allOf": [ + { "$ref": "#/definitions/ObjectNoPropsDatatype" }, + { "minProperties": 1 } + ] + }, + "PatternPropsDatatype": { + "patternProperties": { + "^S_": { "type": "string" } + } + }, + "RefToPatternPropsAllOf": { + "description": "Ref to patternProperties - not a root model.", + "allOf": [ + { "$ref": "#/definitions/PatternPropsDatatype" }, + { "minProperties": 1 } + ] + }, + "NestedAllOfDatatype": { + "allOf": [ + { "type": "string" }, + { "minLength": 1 } + ] + }, + "RefToNestedAllOfAllOf": { + "description": "Ref to nested allOf - not a root model.", + "allOf": [ + { "$ref": "#/definitions/NestedAllOfDatatype" }, + { "maxLength": 100 } + ] + }, + "ConstraintsOnlyDatatype": { + "description": "Constraints only, no type.", + "minLength": 1, + "pattern": "^[A-Z]" + }, + "RefToConstraintsOnlyAllOf": { + "description": "Ref to constraints-only schema.", + "allOf": [ + { "$ref": "#/definitions/ConstraintsOnlyDatatype" }, + { "maxLength": 100 } + ] + }, + "NoDescriptionAllOf": { + "allOf": [ + { "$ref": "#/definitions/StringDatatype" }, + { "minLength": 5 } + ] + }, + "EmptyConstraintItemAllOf": { + "description": "AllOf with empty constraint item.", + "allOf": [ + { "$ref": "#/definitions/StringDatatype" }, + {}, + { "maxLength": 50 } + ] + }, + "ConflictingFormatAllOf": { + "description": "Conflicting formats - falls back to existing behavior.", + "allOf": [ + { "$ref": "#/definitions/FormattedStringDatatype" }, + { "format": "date-time" } + ] + } + }, + "type": "object", + "properties": { + "name": { "$ref": "#/definitions/ConstrainedStringDatatype" }, + "count": { "$ref": "#/definitions/NonNegativeIntegerDatatype" }, + "percentage": { "$ref": "#/definitions/BoundedIntegerDatatype" }, + "email": { "$ref": "#/definitions/EmailDatatype" }, + "obj": { "$ref": "#/definitions/ObjectWithAllOf" }, + "multi": { "$ref": "#/definitions/MultiRefAllOf" }, + "noconstraint": { "$ref": "#/definitions/NoConstraintAllOf" }, + "incompatible": { "$ref": "#/definitions/IncompatibleTypeAllOf" }, + "withprops": { "$ref": "#/definitions/ConstraintWithProperties" }, + "withitems": { "$ref": "#/definitions/ConstraintWithItems" }, + "numint": { "$ref": "#/definitions/NumberIntegerCompatible" }, + "refwithkw": { "$ref": "#/definitions/RefWithSchemaKeywords" }, + "refarr": { "$ref": "#/definitions/RefToArrayAllOf" }, + "refobjnoprops": { "$ref": "#/definitions/RefToObjectNoPropsAllOf" }, + "refpatternprops": { "$ref": "#/definitions/RefToPatternPropsAllOf" }, + "refnestedallof": { "$ref": "#/definitions/RefToNestedAllOfAllOf" }, + "refconstraintsonly": { "$ref": "#/definitions/RefToConstraintsOnlyAllOf" }, + "nodescription": { "$ref": "#/definitions/NoDescriptionAllOf" }, + "emptyconstraint": { "$ref": "#/definitions/EmptyConstraintItemAllOf" }, + "conflictingformat": { "$ref": "#/definitions/ConflictingFormatAllOf" } + } + } ``` ??? example "Output" - ```python - # generated by datamodel-codegen: - # filename: allof_materialize_defaults.yaml - # timestamp: 2019-07-26T00:00:00+00:00 - - from __future__ import annotations - - from typing import Optional - - from pydantic import BaseModel, conint, constr - - - class Parent(BaseModel): - name: Optional[constr(min_length=1)] = 'parent_default' - count: Optional[conint(ge=0)] = 10 - - - class Child(Parent): - name: Optional[constr(min_length=1, max_length=100)] = 'parent_default' - count: Optional[conint(ge=0, le=1000)] = 10 - ``` + === "With Option" + + ```python + # generated by datamodel-codegen: + # filename: allof_root_model_constraints.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Any, Dict, List, Optional + + from pydantic import BaseModel, EmailStr, Field, conint, constr + + + class StringDatatype(BaseModel): + __root__: constr(regex=r'^\S(.*\S)?$') = Field( + ..., description='A base string type.' + ) + + + class ConstrainedStringDatatype(BaseModel): + __root__: constr(regex=r'(?=^\S(.*\S)?$)(?=^[A-Z].*)', min_length=1) = Field( + ..., description='A constrained string.' + ) + + + class IntegerDatatype(BaseModel): + __root__: int = Field(..., description='A whole number.') + + + class NonNegativeIntegerDatatype(BaseModel): + __root__: conint(ge=0) = Field(..., description='Non-negative integer.') + + + class BoundedIntegerDatatype(BaseModel): + __root__: conint(ge=0, le=100) = Field( + ..., description='Integer between 0 and 100.' + ) + + + class EmailDatatype(BaseModel): + __root__: EmailStr = Field(..., description='Email with format.') + + + class FormattedStringDatatype(BaseModel): + __root__: EmailStr = Field(..., description='A string with email format.') + + + class ObjectBase(BaseModel): + id: Optional[int] = None + + + class ObjectWithAllOf(ObjectBase): + name: Optional[str] = None + + + class MultiRefAllOf(BaseModel): + pass + + + class NoConstraintAllOf(BaseModel): + pass + + + class IncompatibleTypeAllOf(BaseModel): + pass + + + class ConstraintWithProperties(BaseModel): + extra: Optional[str] = None + + + class ConstraintWithItems(BaseModel): + pass + + + class NumberIntegerCompatible(BaseModel): + __root__: conint(ge=0) = Field( + ..., description='Number and integer are compatible.' + ) + + + class RefWithSchemaKeywords(BaseModel): + __root__: constr(regex=r'^\S(.*\S)?$', min_length=5, max_length=100) = Field( + ..., description='Ref with additional schema keywords.' + ) + + + class ArrayDatatype(BaseModel): + __root__: List[str] + + + class RefToArrayAllOf(BaseModel): + pass + + + class ObjectNoPropsDatatype(BaseModel): + pass + + + class RefToObjectNoPropsAllOf(ObjectNoPropsDatatype): + pass + + + class PatternPropsDatatype(BaseModel): + __root__: Dict[constr(regex=r'^S_'), str] + + + class RefToPatternPropsAllOf(BaseModel): + pass + + + class NestedAllOfDatatype(BaseModel): + pass + + + class RefToNestedAllOfAllOf(NestedAllOfDatatype): + pass + + + class ConstraintsOnlyDatatype(BaseModel): + __root__: Any = Field(..., description='Constraints only, no type.') + + + class RefToConstraintsOnlyAllOf(BaseModel): + __root__: Any = Field(..., description='Ref to constraints-only schema.') + + + class NoDescriptionAllOf(BaseModel): + __root__: constr(regex=r'^\S(.*\S)?$', min_length=5) = Field( + ..., description='A base string type.' + ) + + + class EmptyConstraintItemAllOf(BaseModel): + __root__: constr(regex=r'^\S(.*\S)?$', max_length=50) = Field( + ..., description='AllOf with empty constraint item.' + ) + + + class ConflictingFormatAllOf(BaseModel): + pass + + + class Model(BaseModel): + name: Optional[ConstrainedStringDatatype] = None + count: Optional[NonNegativeIntegerDatatype] = None + percentage: Optional[BoundedIntegerDatatype] = None + email: Optional[EmailDatatype] = None + obj: Optional[ObjectWithAllOf] = None + multi: Optional[MultiRefAllOf] = None + noconstraint: Optional[NoConstraintAllOf] = None + incompatible: Optional[IncompatibleTypeAllOf] = None + withprops: Optional[ConstraintWithProperties] = None + withitems: Optional[ConstraintWithItems] = None + numint: Optional[NumberIntegerCompatible] = None + refwithkw: Optional[RefWithSchemaKeywords] = None + refarr: Optional[RefToArrayAllOf] = None + refobjnoprops: Optional[RefToObjectNoPropsAllOf] = None + refpatternprops: Optional[RefToPatternPropsAllOf] = None + refnestedallof: Optional[RefToNestedAllOfAllOf] = None + refconstraintsonly: Optional[RefToConstraintsOnlyAllOf] = None + nodescription: Optional[NoDescriptionAllOf] = None + emptyconstraint: Optional[EmptyConstraintItemAllOf] = None + conflictingformat: Optional[ConflictingFormatAllOf] = None + ``` + + === "Without Option" + + ```python + # generated by datamodel-codegen: + # filename: allof_root_model_constraints.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Any, Dict, List, Optional + + from pydantic import BaseModel, EmailStr, Field, conint, constr + + + class StringDatatype(BaseModel): + __root__: constr(regex=r'^\S(.*\S)?$') = Field( + ..., description='A base string type.' + ) + + + class ConstrainedStringDatatype(BaseModel): + __root__: constr(regex=r'^[A-Z].*', min_length=1) = Field( + ..., description='A constrained string.' + ) + + + class IntegerDatatype(BaseModel): + __root__: int = Field(..., description='A whole number.') + + + class NonNegativeIntegerDatatype(BaseModel): + __root__: conint(ge=0) = Field(..., description='Non-negative integer.') + + + class BoundedIntegerDatatype(BaseModel): + __root__: conint(ge=0, le=100) = Field( + ..., description='Integer between 0 and 100.' + ) + + + class EmailDatatype(BaseModel): + __root__: EmailStr = Field(..., description='Email with format.') + + + class FormattedStringDatatype(BaseModel): + __root__: EmailStr = Field(..., description='A string with email format.') + + + class ObjectBase(BaseModel): + id: Optional[int] = None + + + class ObjectWithAllOf(ObjectBase): + name: Optional[str] = None + + + class MultiRefAllOf(BaseModel): + pass + + + class NoConstraintAllOf(BaseModel): + pass + + + class IncompatibleTypeAllOf(BaseModel): + pass + + + class ConstraintWithProperties(BaseModel): + extra: Optional[str] = None + + + class ConstraintWithItems(BaseModel): + pass + + + class NumberIntegerCompatible(BaseModel): + __root__: conint(ge=0) = Field( + ..., description='Number and integer are compatible.' + ) + + + class RefWithSchemaKeywords(BaseModel): + __root__: constr(regex=r'^\S(.*\S)?$', min_length=5, max_length=100) = Field( + ..., description='Ref with additional schema keywords.' + ) + + + class ArrayDatatype(BaseModel): + __root__: List[str] + + + class RefToArrayAllOf(BaseModel): + pass + + + class ObjectNoPropsDatatype(BaseModel): + pass + + + class RefToObjectNoPropsAllOf(ObjectNoPropsDatatype): + pass + + + class PatternPropsDatatype(BaseModel): + __root__: Dict[constr(regex=r'^S_'), str] + + + class RefToPatternPropsAllOf(BaseModel): + pass + + + class NestedAllOfDatatype(BaseModel): + pass + + + class RefToNestedAllOfAllOf(NestedAllOfDatatype): + pass + + + class ConstraintsOnlyDatatype(BaseModel): + __root__: Any = Field(..., description='Constraints only, no type.') + + + class RefToConstraintsOnlyAllOf(BaseModel): + __root__: Any = Field(..., description='Ref to constraints-only schema.') + + + class NoDescriptionAllOf(BaseModel): + __root__: constr(regex=r'^\S(.*\S)?$', min_length=5) = Field( + ..., description='A base string type.' + ) + + + class EmptyConstraintItemAllOf(BaseModel): + __root__: constr(regex=r'^\S(.*\S)?$', max_length=50) = Field( + ..., description='AllOf with empty constraint item.' + ) + + + class ConflictingFormatAllOf(BaseModel): + pass + + + class Model(BaseModel): + name: Optional[ConstrainedStringDatatype] = None + count: Optional[NonNegativeIntegerDatatype] = None + percentage: Optional[BoundedIntegerDatatype] = None + email: Optional[EmailDatatype] = None + obj: Optional[ObjectWithAllOf] = None + multi: Optional[MultiRefAllOf] = None + noconstraint: Optional[NoConstraintAllOf] = None + incompatible: Optional[IncompatibleTypeAllOf] = None + withprops: Optional[ConstraintWithProperties] = None + withitems: Optional[ConstraintWithItems] = None + numint: Optional[NumberIntegerCompatible] = None + refwithkw: Optional[RefWithSchemaKeywords] = None + refarr: Optional[RefToArrayAllOf] = None + refobjnoprops: Optional[RefToObjectNoPropsAllOf] = None + refpatternprops: Optional[RefToPatternPropsAllOf] = None + refnestedallof: Optional[RefToNestedAllOfAllOf] = None + refconstraintsonly: Optional[RefToConstraintsOnlyAllOf] = None + nodescription: Optional[NoDescriptionAllOf] = None + emptyconstraint: Optional[EmptyConstraintItemAllOf] = None + conflictingformat: Optional[ConflictingFormatAllOf] = None + ``` --- @@ -167,12 +663,11 @@ postponed evaluation of annotations (PEP 563). ## `--enum-field-as-literal` {#enum-field-as-literal} -Generate Literal types instead of Enums for fields with enumerated values. +Convert all enum fields to Literal types instead of Enum classes. -The --enum-field-as-literal option replaces Enum classes with Literal types for -fields that have a fixed set of allowed values. Use 'all' to convert all enum -fields, or 'one' to only convert enums with a single value. This produces more -concise type hints and avoids creating Enum classes when not needed. +The `--enum-field-as-literal all` flag converts all enum types to Literal +type annotations. This is useful when you want string literal types instead +of Enum classes for all enumerations. !!! tip "Usage" @@ -184,46 +679,113 @@ concise type hints and avoids creating Enum classes when not needed. ??? example "Input Schema" - ```yaml - $schema: http://json-schema.org/draft-07/schema# - type: object - title: Config - properties: - mode: - title: Mode - type: string - oneOf: - - title: fast - const: fast - - title: slow - const: slow - modes: - type: array - items: - type: string - oneOf: - - const: a - - const: b + ```graphql + "Employee shift status" + enum EmployeeShiftStatus { + "not on shift" + NOT_ON_SHIFT + "on shift" + ON_SHIFT + } + + enum Color { + RED + GREEN + BLUE + } + + enum EnumWithOneField { + FIELD + } ``` ??? example "Output" - ```python - # generated by datamodel-codegen: - # filename: oneof_const_enum_nested.yaml - # timestamp: 2019-07-26T00:00:00+00:00 - - from __future__ import annotations - - from typing import List, Literal, Optional - - from pydantic import BaseModel, Field - - - class Config(BaseModel): - mode: Optional[Literal['fast', 'slow']] = Field(None, title='Mode') - modes: Optional[List[Literal['a', 'b']]] = None - ``` + === "With Option" + + ```python + # generated by datamodel-codegen: + # filename: enums.graphql + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Literal + + from pydantic import BaseModel + from typing_extensions import TypeAlias + + Boolean: TypeAlias = bool + """ + The `Boolean` scalar type represents `true` or `false`. + """ + + + String: TypeAlias = str + """ + The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. + """ + + + class Color(BaseModel): + __root__: Literal['BLUE', 'GREEN', 'RED'] + + + class EmployeeShiftStatus(BaseModel): + """ + Employee shift status + """ + + __root__: Literal['NOT_ON_SHIFT', 'ON_SHIFT'] + + + class EnumWithOneField(BaseModel): + __root__: Literal['FIELD'] + ``` + + === "Without Option" + + ```python + # generated by datamodel-codegen: + # filename: enums.graphql + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from enum import Enum + + from typing_extensions import TypeAlias + + Boolean: TypeAlias = bool + """ + The `Boolean` scalar type represents `true` or `false`. + """ + + + String: TypeAlias = str + """ + The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. + """ + + + class Color(Enum): + BLUE = 'BLUE' + GREEN = 'GREEN' + RED = 'RED' + + + class EmployeeShiftStatus(Enum): + """ + Employee shift status + """ + + NOT_ON_SHIFT = 'NOT_ON_SHIFT' + ON_SHIFT = 'ON_SHIFT' + + + class EnumWithOneField(Enum): + FIELD = 'FIELD' + ``` --- diff --git a/tests/main/graphql/test_main_graphql.py b/tests/main/graphql/test_main_graphql.py index 0539ced48..5929f39af 100644 --- a/tests/main/graphql/test_main_graphql.py +++ b/tests/main/graphql/test_main_graphql.py @@ -27,12 +27,25 @@ ), ], ) +@pytest.mark.cli_doc( + options=["--output-model-type"], + input_schema="graphql/simple-star-wars.graphql", + cli_args=["--output-model-type", "pydantic.BaseModel"], + model_outputs={ + "pydantic_v1": "graphql/simple_star_wars.py", + "dataclass": "graphql/simple_star_wars_dataclass.py", + }, +) @pytest.mark.skipif( black.__version__.split(".")[0] == "19", reason="Installed black doesn't support the old style", ) def test_main_graphql_simple_star_wars(output_model: str, expected_output: str, output_file: Path) -> None: - """Test GraphQL code generation for simple Star Wars schema.""" + """Generate models from GraphQL with different output model types. + + This example demonstrates using `--output-model-type` with GraphQL schemas + to generate either Pydantic models or dataclasses. + """ run_main_and_assert( input_path=GRAPHQL_DATA_PATH / "simple-star-wars.graphql", output_path=output_file, @@ -165,8 +178,20 @@ def test_main_graphql_specialized_enums(output_file: Path) -> None: ) +@pytest.mark.cli_doc( + options=["--enum-field-as-literal"], + input_schema="graphql/enums.graphql", + cli_args=["--enum-field-as-literal", "all"], + golden_output="graphql/enum_literals_all.py", + comparison_output="graphql/enums.py", +) def test_main_graphql_enums_as_literals_all(output_file: Path) -> None: - """Test GraphQL code generation with all enum fields as literals.""" + """Convert all enum fields to Literal types instead of Enum classes. + + The `--enum-field-as-literal all` flag converts all enum types to Literal + type annotations. This is useful when you want string literal types instead + of Enum classes for all enumerations. + """ run_main_and_assert( input_path=GRAPHQL_DATA_PATH / "enums.graphql", output_path=output_file, @@ -177,8 +202,18 @@ def test_main_graphql_enums_as_literals_all(output_file: Path) -> None: ) +@pytest.mark.cli_doc( + options=["--enum-field-as-literal"], + input_schema="graphql/enums.graphql", + cli_args=["--enum-field-as-literal", "one"], + golden_output="graphql/enum_literals_one.py", +) def test_main_graphql_enums_as_literals_one(output_file: Path) -> None: - """Test GraphQL code generation with single-field enums as literals.""" + """Convert single-member enums to Literal types. + + The `--enum-field-as-literal one` flag only converts enums with a single + member to Literal types, keeping multi-member enums as Enum classes. + """ run_main_and_assert( input_path=GRAPHQL_DATA_PATH / "enums.graphql", output_path=output_file, diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index efeb3ee5c..2652d1b80 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -432,12 +432,19 @@ def test_main_jsonschema_no_empty_collapsed_external_model(tmp_path: Path) -> No options=["--output-model-type"], input_schema="jsonschema/null_and_array.json", cli_args=["--output-model-type", "pydantic.BaseModel"], - golden_output="jsonschema/null_and_array.py", + model_outputs={ + "pydantic_v1": "main/jsonschema/null_and_array.py", + "pydantic_v2": "main/jsonschema/null_and_array_v2.py", + }, + primary=True, ) def test_main_null_and_array(output_model: str, expected_output: str, output_file: Path) -> None: - """Specify the output model type (pydantic, dataclass, typeddict, msgspec). + """Select the output model type (Pydantic v1/v2, dataclasses, TypedDict, msgspec). - The `--output-model-type` flag configures the code generation behavior. + The `--output-model-type` flag specifies which Python data model framework to use + for the generated code. Supported values include `pydantic.BaseModel`, + `pydantic_v2.BaseModel`, `dataclasses.dataclass`, `typing.TypedDict`, and + `msgspec.Struct`. """ run_main_and_assert( input_path=JSON_SCHEMA_DATA_PATH / "null_and_array.json", @@ -1944,12 +1951,6 @@ def test_main_jsonschema_field_include_all_keys(output_file: Path) -> None: ) -@pytest.mark.cli_doc( - options=["--field-extra-keys-without-x-prefix"], - input_schema="jsonschema/extras.json", - cli_args=["--field-include-all-keys", "--field-extra-keys-without-x-prefix", "x-repr"], - golden_output="main/jsonschema/field_extras_field_include_all_keys.py", -) @pytest.mark.parametrize( ("output_model", "expected_output"), [ @@ -1963,6 +1964,15 @@ def test_main_jsonschema_field_include_all_keys(output_file: Path) -> None: ), ], ) +@pytest.mark.cli_doc( + options=["--field-extra-keys-without-x-prefix"], + input_schema="jsonschema/extras.json", + cli_args=["--field-include-all-keys", "--field-extra-keys-without-x-prefix", "x-repr"], + model_outputs={ + "pydantic_v1": "main/jsonschema/field_extras_field_include_all_keys.py", + "pydantic_v2": "main/jsonschema/field_extras_field_include_all_keys_v2.py", + }, +) def test_main_jsonschema_field_extras_field_include_all_keys( output_model: str, expected_output: str, output_file: Path ) -> None: @@ -2006,7 +2016,10 @@ def test_main_jsonschema_field_extras_field_include_all_keys( options=["--field-extra-keys"], input_schema="jsonschema/extras.json", cli_args=["--field-extra-keys", "key2", "--field-extra-keys-without-x-prefix", "x-repr"], - golden_output="jsonschema/field_extras_field_extra_keys.py", + model_outputs={ + "pydantic_v1": "main/jsonschema/field_extras_field_extra_keys.py", + "pydantic_v2": "main/jsonschema/field_extras_field_extra_keys_v2.py", + }, ) def test_main_jsonschema_field_extras_field_extra_keys( output_model: str, expected_output: str, output_file: Path @@ -4087,57 +4100,43 @@ def test_main_jsonschema_ref_with_additional_keywords(output_dir: Path) -> None: ) +@pytest.mark.parametrize( + ("output_model", "expected_file"), + [ + ("typing.TypedDict", "reserved_field_name_schema_typed_dict.py"), + ("dataclasses.dataclass", "reserved_field_name_schema_dataclass.py"), + ("pydantic_v2.BaseModel", "reserved_field_name_schema_pydantic.py"), + ], +) +@pytest.mark.cli_doc( + options=["--output-model-type"], + input_schema="jsonschema/reserved_field_name_schema.json", + cli_args=["--target-python-version", "3.11"], + model_outputs={ + "typeddict": "main/jsonschema/reserved_field_name_schema_typed_dict.py", + "dataclass": "main/jsonschema/reserved_field_name_schema_dataclass.py", + "pydantic_v2": "main/jsonschema/reserved_field_name_schema_pydantic.py", + }, +) @pytest.mark.benchmark @LEGACY_BLACK_SKIP -def test_main_jsonschema_reserved_field_name_typed_dict(output_file: Path) -> None: - """Test that 'schema' field is not renamed in TypedDict (Issue #1833).""" - run_main_and_assert( - input_path=JSON_SCHEMA_DATA_PATH / "reserved_field_name_schema.json", - output_path=output_file, - input_file_type="jsonschema", - assert_func=assert_file_content, - expected_file="reserved_field_name_schema_typed_dict.py", - extra_args=[ - "--output-model-type", - "typing.TypedDict", - "--target-python-version", - "3.11", - ], - ) - - -@pytest.mark.benchmark -@LEGACY_BLACK_SKIP -def test_main_jsonschema_reserved_field_name_dataclass(output_file: Path) -> None: - """Test that 'schema' field is not renamed in dataclass (Issue #1833).""" - run_main_and_assert( - input_path=JSON_SCHEMA_DATA_PATH / "reserved_field_name_schema.json", - output_path=output_file, - input_file_type="jsonschema", - assert_func=assert_file_content, - expected_file="reserved_field_name_schema_dataclass.py", - extra_args=[ - "--output-model-type", - "dataclasses.dataclass", - "--target-python-version", - "3.11", - ], - ) - +def test_main_jsonschema_reserved_field_name(output_model: str, expected_file: str, output_file: Path) -> None: + """Test reserved field name handling across model types (Issue #1833). -@pytest.mark.benchmark -@LEGACY_BLACK_SKIP -def test_main_jsonschema_reserved_field_name_pydantic(output_file: Path) -> None: - """Test that 'schema' field is renamed to 'schema_' with alias in Pydantic (Issue #1833).""" + This demonstrates how 'schema' field is handled: + - TypedDict: not renamed (schema is not reserved) + - dataclass: not renamed (schema is not reserved) + - Pydantic: renamed to 'schema_' with alias (BaseModel.schema conflicts) + """ run_main_and_assert( input_path=JSON_SCHEMA_DATA_PATH / "reserved_field_name_schema.json", output_path=output_file, input_file_type="jsonschema", assert_func=assert_file_content, - expected_file="reserved_field_name_schema_pydantic.py", + expected_file=expected_file, extra_args=[ "--output-model-type", - "pydantic_v2.BaseModel", + output_model, "--target-python-version", "3.11", ], @@ -4200,47 +4199,41 @@ def test_main_bundled_schema_with_id_url(mocker: MockerFixture, output_file: Pat ) +@pytest.mark.parametrize( + ("output_model", "expected_file"), + [ + ("pydantic.BaseModel", "use_frozen_field_v1.py"), + ("pydantic_v2.BaseModel", "use_frozen_field_v2.py"), + ("dataclasses.dataclass", "use_frozen_field_dataclass.py"), + ], +) @pytest.mark.cli_doc( options=["--use-frozen-field"], input_schema="jsonschema/use_frozen_field.json", - cli_args=["--use-frozen-field", "--output-model-type", "pydantic_v2.BaseModel"], - golden_output="jsonschema/use_frozen_field_v2.py", + cli_args=["--use-frozen-field"], + model_outputs={ + "pydantic_v1": "main/jsonschema/use_frozen_field_v1.py", + "pydantic_v2": "main/jsonschema/use_frozen_field_v2.py", + "dataclass": "main/jsonschema/use_frozen_field_dataclass.py", + }, ) @pytest.mark.benchmark @LEGACY_BLACK_SKIP -def test_main_use_frozen_field_pydantic_v2(output_file: Path) -> None: - """Generate frozen (immutable) field definitions. +def test_main_use_frozen_field(output_model: str, expected_file: str, output_file: Path) -> None: + """Generate frozen (immutable) field definitions for readOnly properties. - The `--use-frozen-field` flag configures the code generation behavior. + The `--use-frozen-field` flag generates frozen field definitions: + - Pydantic v1: `Field(allow_mutation=False)` + - Pydantic v2: `Field(frozen=True)` + - Dataclasses: silently ignored (no frozen fields generated) """ run_main_and_assert( input_path=JSON_SCHEMA_DATA_PATH / "use_frozen_field.json", output_path=output_file, input_file_type="jsonschema", assert_func=assert_file_content, - expected_file="use_frozen_field_v2.py", - extra_args=[ - "--output-model-type", - "pydantic_v2.BaseModel", - "--use-frozen-field", - ], - ) - - -@pytest.mark.benchmark -def test_main_use_frozen_field_pydantic_v1(output_file: Path) -> None: - """Test --use-frozen-field with Pydantic v1 generates Field(allow_mutation=False) for readOnly fields.""" - run_main_and_assert( - input_path=JSON_SCHEMA_DATA_PATH / "use_frozen_field.json", - output_path=output_file, - input_file_type="jsonschema", - assert_func=assert_file_content, - expected_file="use_frozen_field_v1.py", - extra_args=[ - "--output-model-type", - "pydantic.BaseModel", - "--use-frozen-field", - ], + expected_file=expected_file, + extra_args=["--output-model-type", output_model, "--use-frozen-field"], ) @@ -4254,28 +4247,7 @@ def test_main_use_frozen_field_no_readonly(output_file: Path) -> None: input_file_type="jsonschema", assert_func=assert_file_content, expected_file="use_frozen_field_no_readonly.py", - extra_args=[ - "--output-model-type", - "pydantic_v2.BaseModel", - "--use-frozen-field", - ], - ) - - -@pytest.mark.benchmark -def test_main_use_frozen_field_dataclass(output_file: Path) -> None: - """Test --use-frozen-field with dataclass silently ignores (no error, no frozen).""" - run_main_and_assert( - input_path=JSON_SCHEMA_DATA_PATH / "use_frozen_field.json", - output_path=output_file, - input_file_type="jsonschema", - assert_func=assert_file_content, - expected_file="use_frozen_field_dataclass.py", - extra_args=[ - "--output-model-type", - "dataclasses.dataclass", - "--use-frozen-field", - ], + extra_args=["--output-model-type", "pydantic_v2.BaseModel", "--use-frozen-field"], ) @@ -4294,27 +4266,40 @@ def test_main_field_name_shadows_class_name(output_file: Path) -> None: ) +@pytest.mark.cli_doc( + options=["--allof-merge-mode"], + input_schema="jsonschema/allof_root_model_constraints.json", + cli_args=["--allof-merge-mode", "constraints"], + golden_output="main/jsonschema/allof_root_model_constraints_merge.py", + comparison_output="main/jsonschema/allof_root_model_constraints.py", +) @pytest.mark.benchmark -def test_main_allof_root_model_constraints(output_file: Path) -> None: - """Test allOf with root model reference and constraints (issue #1901).""" +def test_main_allof_root_model_constraints_merge(output_file: Path) -> None: + """Merge constraints from root model references in allOf schemas. + + The `--allof-merge-mode constraints` merges only constraint properties + (minLength, maximum, etc.) from parent schemas referenced in allOf. + This ensures child schemas inherit validation constraints while keeping + other properties separate. + """ run_main_and_assert( input_path=JSON_SCHEMA_DATA_PATH / "allof_root_model_constraints.json", output_path=output_file, input_file_type="jsonschema", assert_func=assert_file_content, - expected_file="allof_root_model_constraints.py", - extra_args=["--allof-merge-mode", "none"], + expected_file="allof_root_model_constraints_merge.py", + extra_args=["--allof-merge-mode", "constraints"], ) @pytest.mark.benchmark -def test_main_allof_root_model_constraints_merge(output_file: Path) -> None: - """Test allOf with root model reference and constraints with merge mode (issue #1901).""" +def test_main_allof_root_model_constraints_none(output_file: Path) -> None: + """Test allOf with root model reference without merging (issue #1901).""" run_main_and_assert( input_path=JSON_SCHEMA_DATA_PATH / "allof_root_model_constraints.json", output_path=output_file, input_file_type="jsonschema", assert_func=assert_file_content, - expected_file="allof_root_model_constraints_merge.py", - extra_args=["--allof-merge-mode", "constraints"], + expected_file="allof_root_model_constraints.py", + extra_args=["--allof-merge-mode", "none"], ) diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 93c1fa915..67d419379 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -2861,27 +2861,38 @@ def test_main_openapi_msgspec_use_annotated_with_field_constraints(output_file: ) -def test_main_openapi_discriminator_one_literal_as_default(output_file: Path) -> None: - """Test OpenAPI generation with discriminator one literal as default.""" - run_main_and_assert( - input_path=OPEN_API_DATA_PATH / "discriminator_enum.yaml", - output_path=output_file, - input_file_type="openapi", - assert_func=assert_file_content, - expected_file=EXPECTED_OPENAPI_PATH / "discriminator" / "enum_one_literal_as_default.py", - extra_args=["--output-model-type", "pydantic_v2.BaseModel", "--use-one-literal-as-default"], - ) - +@pytest.mark.parametrize( + ("output_model", "expected_file"), + [ + ("pydantic_v2.BaseModel", "discriminator/enum_one_literal_as_default.py"), + ("dataclasses.dataclass", "discriminator/dataclass_enum_one_literal_as_default.py"), + ], +) +@pytest.mark.cli_doc( + options=["--use-one-literal-as-default"], + input_schema="openapi/discriminator_enum.yaml", + cli_args=["--use-one-literal-as-default"], + model_outputs={ + "pydantic_v2": "openapi/discriminator/enum_one_literal_as_default.py", + "dataclass": "openapi/discriminator/dataclass_enum_one_literal_as_default.py", + }, +) +def test_main_openapi_discriminator_one_literal_as_default( + output_model: str, expected_file: str, output_file: Path +) -> None: + """Set default value when only one literal is valid for a discriminator field. -def test_main_openapi_discriminator_one_literal_as_default_dataclass(output_file: Path) -> None: - """Test OpenAPI generation with discriminator one literal as default for dataclass.""" + The `--use-one-literal-as-default` flag sets default values for discriminator + fields when only one literal value is valid, reducing boilerplate in model + instantiation. + """ run_main_and_assert( input_path=OPEN_API_DATA_PATH / "discriminator_enum.yaml", output_path=output_file, input_file_type="openapi", assert_func=assert_file_content, - expected_file=EXPECTED_OPENAPI_PATH / "discriminator" / "dataclass_enum_one_literal_as_default.py", - extra_args=["--output-model-type", "dataclasses.dataclass", "--use-one-literal-as-default"], + expected_file=EXPECTED_OPENAPI_PATH / expected_file, + extra_args=["--output-model-type", output_model, "--use-one-literal-as-default"], ) diff --git a/tests/test_main_kr.py b/tests/test_main_kr.py index ea66925d3..d0efb0a38 100644 --- a/tests/test_main_kr.py +++ b/tests/test_main_kr.py @@ -278,9 +278,21 @@ def test_main_use_schema_description(output_file: Path) -> None: ) +@pytest.mark.cli_doc( + options=["--use-field-description"], + input_schema="openapi/api_multiline_docstrings.yaml", + cli_args=["--use-field-description"], + golden_output="main_kr/main_use_field_description/output.py", + related_options=["--use-schema-description", "--use-inline-field-description"], +) @freeze_time("2022-11-11") def test_main_use_field_description(output_file: Path) -> None: - """Test --use-field-description option.""" + """Add field descriptions using Pydantic Field(). + + The `--use-field-description` flag adds the `description` property from + schema fields as the `description` parameter in Pydantic Field(). This + provides documentation that is accessible via model schema and OpenAPI docs. + """ run_main_and_assert( input_path=OPEN_API_DATA_PATH / "api_multiline_docstrings.yaml", output_path=output_file,