Repository: pydantic/pydantic
Pull request: Fix pydantic dataclass problem with dataclasses.field default
Fixes https://github.com/pydantic/pydantic/issues/7895

Selected Reviewer: @dmontagu

Problem statement:
Inconsistent `model_dump` behavior with dataclasses and `field(init=False)`
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

Models will only dump correctly if a field with `init=False` uses `default_factory`.

### Example Code

```Python
import uuid
from pydantic import RootModel
from pydantic.dataclasses import dataclass
from dataclasses import field

@dataclass
class Works:
    id: str = field(init=False, default_factory=lambda: str(uuid.uuid4()))

@dataclass
class DoesntWork1:
    id: str = field(init=False, default="DEFAULT_ID")

@dataclass
class DoesntWork2:
    id: str = field(init=False)

    def __post_init__(self):
        self.id = str(uuid.uuid4())

w = Works()
dw1 = DoesntWork1()
dw2 = DoesntWork2()

print(RootModel[Works](w).model_dump_json())
print(RootModel[DoesntWork1](dw1).model_dump_json())
print(RootModel[DoesntWork2](dw2).model_dump_json())

# Result:
# {"id":"0d984922-443f-4b0d-999e-e91185791ef2"}
# {}
# {}
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.4.2
        pydantic-core version: 2.10.1
          pydantic-core build: profile=release pgo=false
                 install path: .../.venv/lib/python3.12/site-packages/pydantic
               python version: 3.12.0 (main, Oct  2 2023, 22:40:56) [Clang 15.0.0 (clang-1500.0.40.1)]
                     platform: macOS-13.6-arm64-arm-64bit
             related packages: mypy-1.6.1 typing_extensions-4.8.0 fastapi-0.104.0
```

Hints from issue discussion:
@PrettyWood could this be somehow related to #7821 ?
No nothing to do with that
It comes from https://github.com/pydantic/pydantic/blob/main/pydantic/_internal/_fields.py#L251
I reckon https://github.com/pydantic/pydantic/pull/6616 introduced a regression on that
because we now have
```py
@dataclass
class A:
    id: str = field(init=False, default_factory=lambda: str(uuid.uuid4()))

@dataclass
class B:
    id: str = field(init=False, default="DEFAULT_ID")

print(A.__pydantic_fields__)
print(B.__pydantic_fields__) 
# {'id': FieldInfo(annotation=str, required=False, default_factory=<lambda>, init_var=False, kw_only=False)}
# {}
```

Commit message: Fix pydantic dataclass problem with dataclasses.field default

Patch to review:
diff --git a/pydantic/_internal/_fields.py b/pydantic/_internal/_fields.py
index e42c7a47e9f..1c2b88f21d7 100644
--- a/pydantic/_internal/_fields.py
+++ b/pydantic/_internal/_fields.py
@@ -248,7 +248,11 @@ def collect_dataclass_fields(
         if is_classvar(ann_type):
             continue
 
-        if not dataclass_field.init and dataclass_field.default_factory == dataclasses.MISSING:
+        if (
+            not dataclass_field.init
+            and dataclass_field.default_factory == dataclasses.MISSING
+            and dataclass_field.default == dataclasses.MISSING
+        ):
             # TODO: We should probably do something with this so that validate_assignment behaves properly
             #   Issue: https://github.com/pydantic/pydantic/issues/5470
             continue
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
index 436a8c06666..0f3d1d6dd50 100644
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -24,6 +24,7 @@
     PydanticDeprecatedSince20,
     PydanticUndefinedAnnotation,
     PydanticUserError,
+    RootModel,
     TypeAdapter,
     ValidationError,
     ValidationInfo,
@@ -2459,7 +2460,21 @@ def test_dataclass_field_default_factory_with_init():
     class Model:
         x: int = dataclasses.field(default_factory=lambda: 3, init=False)
 
-    assert Model().x == 3
+    m = Model()
+    assert 'x' in Model.__pydantic_fields__
+    assert m.x == 3
+    assert RootModel[Model](m).model_dump() == {'x': 3}
+
+
+def test_dataclass_field_default_with_init():
+    @pydantic.dataclasses.dataclass
+    class Model:
+        x: int = dataclasses.field(default=3, init=False)
+
+    m = Model()
+    assert 'x' in Model.__pydantic_fields__
+    assert m.x == 3
+    assert RootModel[Model](m).model_dump() == {'x': 3}
 
 
 def test_metadata():


Code review comments:
diff_hunk: @@ -248,7 +248,11 @@ def collect_dataclass_fields(
         if is_classvar(ann_type):
             continue
 
-        if not dataclass_field.init and dataclass_field.default_factory == dataclasses.MISSING:
+        if (
+            not dataclass_field.init
+            and dataclass_field.default_factory == dataclasses.MISSING
+            and dataclass_field.default == dataclasses.MISSING
path: pydantic/_internal/_fields.py
text: @user1:
I would have swapped the order because `default` is used more often that `default_factory`

@author:
Sure! Done