Repository: koxudaxi/datamodel-code-generator

Issue:
msgspec: Optional fields are missing a default when using `--snake-case-field`
### Steps to reproduce
1. Download the NVD CVE [schema][schema]
2. Generate a msgpsec model:
```sh
datamodel-codegen \
    --input $schema_json \
    --input-file-type jsonschema \
    --output-model-type 'msgspec.Struct' \
   # This is important I think
    --snake-case-field \
    --output "."
```
3. (Ignore the circular imports #836)
4. (Ignore wrong field ordering #1919 
5. Look at the `class CpeMatch` (and most other classes as well).
```python
class CpeMatch(Struct, kw_only=True):
    vulnerable: bool
    criteria: str
    match_criteria_id: str = field(name='matchCriteriaId')
    version_start_excluding: Optional[str] = field(name='versionStartExcluding')
    version_start_including: Optional[str] = field(name='versionStartIncluding')
    version_end_excluding: Optional[str] = field(name='versionEndExcluding')
    version_end_including: Optional[str] = field(name='versionEndIncluding')

```
vs
```json

"cpe_match": {
	"description": "CPE match string or range",
	"type": "object",
	"properties": {
		"vulnerable": {"type": "boolean"},
		"criteria": {"type": "string"},
		"matchCriteriaId": {"type": "string", "format": "uuid"},
		"versionStartExcluding": {"type": "string"},
		"versionStartIncluding": {"type": "string"},
		"versionEndExcluding": {"type": "string"},
		"versionEndIncluding": {"type": "string"}
	},
	"required": ["vulnerable", "criteria", "matchCriteriaId"],
	"additionalProperties": false
},
```

Note that the optional fields are missing the `default=None` parameter in the `field` call.

[schema]: https://csrc.nist.gov/schema/nvd/api/2.0/cve_api_json_2.0.schema

### Expected behavior
The field should have a default of value `None`.

### Workaround
Do not use `--snake-case-field`.

### Setup

```sh
$ datamodel-codegen --version
0.25.5

$ python --version
Python 3.11.8
```



Discussion:

Resolving patch:
diff --git a/datamodel_code_generator/model/msgspec.py b/datamodel_code_generator/model/msgspec.py
--- a/datamodel_code_generator/model/msgspec.py
+++ b/datamodel_code_generator/model/msgspec.py
@@ -33,7 +33,7 @@
 
 
 def _has_field_assignment(field: DataModelFieldBase) -> bool:
-    return bool(field.field) or not (
+    return not (
         field.required
         or (field.represented_default == 'None' and field.strip_default_none)
     )
@@ -48,7 +48,9 @@ def import_extender(cls: Type[DataModelFieldBaseT]) -> Type[DataModelFieldBaseT]
     @wraps(original_imports.fget)  # type: ignore
     def new_imports(self: DataModelFieldBaseT) -> Tuple[Import, ...]:
         extra_imports = []
-        if self.field:
+        field = self.field
+        # TODO: Improve field detection
+        if field and field.startswith('field('):
             extra_imports.append(IMPORT_MSGSPEC_FIELD)
         if self.field and 'lambda: convert' in self.field:
             extra_imports.append(IMPORT_MSGSPEC_CONVERT)
@@ -177,6 +179,8 @@ def __str__(self) -> str:
 
         if self.default != UNDEFINED and self.default is not None:
             data['default'] = self.default
+        elif not self.required:
+            data['default'] = None
 
         if self.required:
             data = {


Test patch:
diff --git a/tests/data/expected/main/main_msgspec_struct_snake_case/output.py b/tests/data/expected/main/main_msgspec_struct_snake_case/output.py
new file mode 100644
--- /dev/null
+++ b/tests/data/expected/main/main_msgspec_struct_snake_case/output.py
@@ -0,0 +1,66 @@
+# generated by datamodel-codegen:
+#   filename:  api_ordered_required_fields.yaml
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from typing import List, Optional
+
+from msgspec import Meta, Struct, field
+from typing_extensions import Annotated
+
+
+class Pet(Struct):
+    id: int
+    name: str
+    before_tag: str = field(name='beforeTag')
+    tag: Optional[str] = None
+
+
+Pets = List[Pet]
+
+
+class User(Struct):
+    id: int
+    name: str
+    tag: Optional[str] = None
+
+
+Users = List[User]
+
+
+Id = str
+
+
+Rules = List[str]
+
+
+class Error(Struct):
+    code: int
+    message: str
+
+
+class Api(Struct):
+    api_key: Optional[
+        Annotated[str, Meta(description='To be used as a dataset parameter value')]
+    ] = field(name='apiKey', default=None)
+    api_version_number: Optional[
+        Annotated[str, Meta(description='To be used as a version parameter value')]
+    ] = field(name='apiVersionNumber', default=None)
+    api_url: Optional[
+        Annotated[str, Meta(description="The URL describing the dataset's fields")]
+    ] = field(name='apiUrl', default=None)
+    api_documentation_url: Optional[
+        Annotated[str, Meta(description='A URL to the API console for each API')]
+    ] = field(name='apiDocumentationUrl', default=None)
+
+
+Apis = List[Api]
+
+
+class Event(Struct):
+    name: Optional[str] = None
+
+
+class Result(Struct):
+    event: Optional[Event] = None
diff --git a/tests/data/expected/main/main_with_aliases_msgspec/output.py b/tests/data/expected/main/main_with_aliases_msgspec/output.py
--- a/tests/data/expected/main/main_with_aliases_msgspec/output.py
+++ b/tests/data/expected/main/main_with_aliases_msgspec/output.py
@@ -57,7 +57,7 @@ class Api(Struct):
 
 
 class Event(Struct):
-    name_: Optional[str] = field(name='name')
+    name_: Optional[str] = field(name='name', default=None)
 
 
 class Result(Struct):
diff --git a/tests/data/openapi/api_ordered_required_fields.yaml b/tests/data/openapi/api_ordered_required_fields.yaml
new file mode 100644
--- /dev/null
+++ b/tests/data/openapi/api_ordered_required_fields.yaml
@@ -0,0 +1,182 @@
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+servers:
+  - url: http://petstore.swagger.io/v1
+paths:
+  /pets:
+    get:
+      summary: List all pets
+      operationId: listPets
+      tags:
+        - pets
+      parameters:
+        - name: limit
+          in: query
+          description: How many items to return at one time (max 100)
+          required: false
+          schema:
+            type: integer
+            format: int32
+      responses:
+        '200':
+          description: A paged array of pets
+          headers:
+            x-next:
+              description: A link to the next page of responses
+              schema:
+                type: string
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Pets"
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+                x-amazon-apigateway-integration:
+                  uri:
+                    Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations
+                  passthroughBehavior: when_no_templates
+                  httpMethod: POST
+                  type: aws_proxy
+    post:
+      summary: Create a pet
+      operationId: createPets
+      tags:
+        - pets
+      responses:
+        '201':
+          description: Null response
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+                x-amazon-apigateway-integration:
+                  uri:
+                    Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations
+                  passthroughBehavior: when_no_templates
+                  httpMethod: POST
+                  type: aws_proxy
+  /pets/{petId}:
+    get:
+      summary: Info for a specific pet
+      operationId: showPetById
+      tags:
+        - pets
+      parameters:
+        - name: petId
+          in: path
+          required: true
+          description: The id of the pet to retrieve
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Expected response to a valid request
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Pets"
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+    x-amazon-apigateway-integration:
+      uri:
+        Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations
+      passthroughBehavior: when_no_templates
+      httpMethod: POST
+      type: aws_proxy
+components:
+  schemas:
+    Pet:
+      required:
+        - id
+        - name
+        - beforeTag
+      properties:
+        id:
+          type: integer
+          format: int64
+          default: 1
+        name:
+          type: string
+        beforeTag:
+          type: string
+        tag:
+          type: string
+    Pets:
+      type: array
+      items:
+        $ref: "#/components/schemas/Pet"
+    Users:
+      type: array
+      items:
+        required:
+          - id
+          - name
+        properties:
+          id:
+            type: integer
+            format: int64
+          name:
+            type: string
+          tag:
+            type: string
+    Id:
+      type: string
+    Rules:
+      type: array
+      items:
+        type: string
+    Error:
+      description: error result
+      required:
+        - code
+        - message
+      properties:
+        code:
+          type: integer
+          format: int32
+        message:
+          type: string
+    apis:
+      type: array
+      items:
+        type: object
+        properties:
+          apiKey:
+            type: string
+            description: To be used as a dataset parameter value
+          apiVersionNumber:
+            type: string
+            description: To be used as a version parameter value
+          apiUrl:
+            type: string
+            format: uri
+            description: "The URL describing the dataset's fields"
+          apiDocumentationUrl:
+            type: string
+            format: uri
+            description: A URL to the API console for each API
+    Event:
+      type: object
+      description: Event object
+      properties:
+        name:
+          type: string
+    Result:
+        type: object
+        properties:
+          event:
+            $ref: '#/components/schemas/Event'
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -6139,6 +6139,33 @@ def test_main_msgspec_struct():
         )
 
 
+@freeze_time('2019-07-26')
+def test_main_msgspec_struct_snake_case():
+    with TemporaryDirectory() as output_dir:
+        output_file: Path = Path(output_dir) / 'output.py'
+        return_code: Exit = main(
+            [
+                '--input',
+                str(OPEN_API_DATA_PATH / 'api_ordered_required_fields.yaml'),
+                '--output',
+                str(output_file),
+                # min msgspec python version is 3.8
+                '--target-python-version',
+                '3.8',
+                '--snake-case-field',
+                '--output-model-type',
+                'msgspec.Struct',
+            ]
+        )
+        assert return_code == Exit.OK
+        assert (
+            output_file.read_text()
+            == (
+                EXPECTED_MAIN_PATH / 'main_msgspec_struct_snake_case' / 'output.py'
+            ).read_text()
+        )
+
+
 @freeze_time('2019-07-26')
 @pytest.mark.skipif(
     black.__version__.split('.')[0] == '19',