Repository: pydantic/pydantic

Issue:
V2 dataclass improvements
* make sure valididators are working on vanilla dataclasses as fields
* make sure `kw_only` is correctly inherited
* make sure serialization schema is right

Fix

```py
from datetime import datetime

from pydantic import field_validator
from pydantic.dataclasses import dataclass


@dataclass
class DemoDataclass:
    ts: datetime = None

    @field_validator('ts', mode='before')
    def set_ts_now(cls, v):
        return v or datetime.now()


print(DemoDataclass())
print(DemoDataclass(ts='2017-11-08T14:00'))
```

Assigned myself, but happy for someone else to work on this.


Discussion:
> make sure valididators are working on vanilla dataclasses as fields

They are, not sure what you meant by this.

> make sure kw_only is correctly inherited

I believe this works, here's a quick test. Is there something specific that wasn't working?

```python
from pydantic.dataclasses import dataclass


@dataclass(kw_only=True)
class Parent:
    x: int


@dataclass
class Child(Parent):
    y: int


print(Child(x=1, y=1))
print(Child(1, x=1))
print(Child(1, 1))
"""
y
  Field required [type=missing, input_value=ArgsKwargs((1,), {'x': 1}), input_type=ArgsKwargs]
"""
```

> make sure the serialization schema is right

This is quite vague, dataclass serialization works in general.

---

@samuelcolvin since it seems like all of this works I'd like to close this as completed, unless you have any more specifics on the issues listed.
@adriangb I think this is what is meant by the first bullet:
```python
from datetime import datetime

from pydantic import field_validator, BaseModel
from dataclasses import dataclass

@dataclass
class DemoDataclass:
    ts: datetime = None

    @field_validator('ts', mode='before')
    def set_ts_now(cls, v):
        return v or datetime.now()


class Model(BaseModel):
    x: DemoDataclass


print(Model(x={'ts': None}))
"""
pydantic_core._pydantic_core.ValidationError: 1 validation error for Model
x.ts
  Input should be a valid datetime [type=datetime_type, input_value=None, input_type=NoneType]
    For further information visit https://errors.pydantic.dev/0.33.0/v/datetime_type
"""
```
(The error goes away with `@pydantic.dataclasses.dataclass`.)
Does that work in V1? I don’t think it should work or we should support it, it could be brittle to support field validators on non pydantic dataclasses with inheritance and whatnot. If it doesn’t work in V1 I don’t think we should make it work in V2 and certainly it should not be a blocker for the release.

The type hint there is also wrong (the default is not a valid value).
If you look at the "fix" in #5857, it seems pretty trivial to support. I think this is a legitimate use case because it lets you use vanilla dataclasses in your python code (i.e., no validation overhead, etc.) but still get validators/serializers applied when used with pydantic.
I saw the fix, it does look simple but I’d like to see it pass all of the inheritance tests and whatnot before we declare it as fixed. We should also at least document for the record what the V1 behavior is.
Also, one interesting thing I found with regard to kw_only:

```python
from pydantic.dataclasses import dataclass


@dataclass(kw_only=True)
class Parent:
    x: int


@dataclass
class Child(Parent):
    y: int


print(Child(1, x=1))
```
This errors, but if you change it to `from dataclasses import dataclass` it stops erroring. I understand what's going on, and I'm not saying there's not an argument for raising the error we raise, but there _is_ an inconsistency. Other than that, I didn't see any issues with kw_only.
That’s does seem wrong to me. Is it not fixable? If it is I think we should respect the behavior of vanilla dataclasses. In any case, going in that direction would not be a breaking change so we can do it later.
@adriangb You are right, my proposed change doesn't work with inheritance.

Interestingly, I got burned by this behavior, I'm not sure whether this should be considered a bug though:
```python
from datetime import datetime

import pydantic.dataclasses
from pydantic import field_validator, TypeAdapter


@pydantic.dataclasses.dataclass
class Demo1:
    ts1: datetime

    @field_validator('ts1', mode='before')
    def set_ts_now(cls, v):
        return datetime.now()


@pydantic.dataclasses.dataclass
class Demo2(Demo1):
    ts2: datetime

    @field_validator('ts2', mode='before')
    def set_ts_now(cls, v):
        return datetime.now()


adapter = TypeAdapter(Demo2)
print(adapter.validate_python(dict(ts1=datetime(2000, 1, 1), ts2=datetime(2000, 1, 1))))
#> Demo2(ts1=datetime.datetime(2000, 1, 1, 0, 0), ts2=datetime.datetime(2023, 5, 24, 14, 7, 25, 30825))
```

(The issue is that because I didn't rename the function, it stopped applying to the `ts1` field from the parent class. I guess maybe this behavior is necessary to enable certain workflows..)
That seems right to me, although I thought we had a warning for it. Otherwise I think we should have a warning, if possible, but I'm not sure it's a big deal.
I think we don't warn because it's in a subclass
But if we detect that you’re dropping validation for a field we maybe could? Generally adding behavior to a subclass (eg overwriting a field to accept another type and thus changing the field validator to accept that type) makes sense, but what you’re showing seems like it would only happen by accident and maybe even violates OOP principles.
@adriangb in #5857 I've made some more changes that makes it work with inheritance. It makes one existing test fail but I would argue that test was more for documentation purposes than desirable behavior. (I also added a test for inheritance.)

I personally think #5857 is an improvement for the same reasons I said above (namely, you can get pydantic decorators for use with TypeAdapter/etc. without actually using a pydantic dataclass), and implemented because I think it addresses the point raised in this issue. If we change our mind about whether it should be supported we can just close it but I figured I would get it "over the line" rather than stopping halfway.
@samuelcolvin not sure what you meant by check "serialization schema" — is it enough to just make sure that things like `field_serializer` work when using `TypeAdapter`?

Patch:
diff --git a/pydantic/_internal/_dataclasses.py b/pydantic/_internal/_dataclasses.py
--- a/pydantic/_internal/_dataclasses.py
+++ b/pydantic/_internal/_dataclasses.py
@@ -174,7 +174,7 @@ class B(A):
 
 
 def is_pydantic_dataclass(_cls: type[Any]) -> TypeGuard[type[PydanticDataclass]]:
-    return dataclasses.is_dataclass(_cls) and hasattr(_cls, '__pydantic_validator__')
+    return dataclasses.is_dataclass(_cls) and '__pydantic_validator__' in _cls.__dict__
 
 
 def rebuild_dataclass(
diff --git a/pydantic/_internal/_decorators.py b/pydantic/_internal/_decorators.py
--- a/pydantic/_internal/_decorators.py
+++ b/pydantic/_internal/_decorators.py
@@ -240,7 +240,7 @@ def build(model_dc: type[Any]) -> DecoratorInfos:  # noqa: C901 (ignore complexi
         # reminder: dicts are ordered and replacement does not alter the order
         res = DecoratorInfos()
         for base in model_dc.__bases__[::-1]:
-            existing = cast(Union[DecoratorInfos, None], getattr(base, '__pydantic_decorators__', None))
+            existing = cast(Union[DecoratorInfos, None], base.__dict__.get('__pydantic_decorators__'))
             if existing is None:
                 existing = DecoratorInfos.build(base)
             res.validators.update({k: v.bind_to_cls(model_dc) for k, v in existing.validators.items()})
diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py
--- a/pydantic/_internal/_generate_schema.py
+++ b/pydantic/_internal/_generate_schema.py
@@ -920,7 +920,7 @@ def _dataclass_schema(
                 self.types_namespace,
                 typevars_map=typevars_map,
             )
-        decorators = getattr(dataclass, '__pydantic_decorators__', None) or DecoratorInfos.build(dataclass)
+        decorators = dataclass.__dict__.get('__pydantic_decorators__') or DecoratorInfos.build(dataclass)
         # Move kw_only=False args to the start of the list, as this is how vanilla dataclasses work.
         # Note that when kw_only is missing or None, it is treated as equivalent to kw_only=True
         args = sorted(
diff --git a/pydantic/type_adapter.py b/pydantic/type_adapter.py
--- a/pydantic/type_adapter.py
+++ b/pydantic/type_adapter.py
@@ -76,6 +76,23 @@ def _get_schema(type_: Any, config_wrapper: _config.ConfigWrapper, parent_depth:
     return gen.generate_schema(type_)
 
 
+def _getattr_no_parents(obj: Any, attribute: str) -> Any:
+    """
+    Returns the attribute value without attempting to look up attributes from parent types
+    """
+    if hasattr(obj, '__dict__'):
+        try:
+            return obj.__dict__[attribute]
+        except KeyError:
+            pass
+
+    slots = getattr(obj, '__slots__', None)
+    if slots is not None and attribute in slots:
+        return getattr(obj, attribute)
+    else:
+        raise AttributeError(attribute)
+
+
 class TypeAdapter(Generic[T]):
     """A class representing the type adapter.
 
@@ -107,7 +124,7 @@ def __init__(self, __type: Any, *, config: ConfigDict | None = None, _parent_dep
 
         core_schema: CoreSchema
         try:
-            core_schema = __type.__pydantic_core_schema__
+            core_schema = _getattr_no_parents(__type, '__pydantic_core_schema__')
         except AttributeError:
             core_schema = _get_schema(__type, config_wrapper, parent_depth=_parent_depth + 1)
 
@@ -116,15 +133,15 @@ def __init__(self, __type: Any, *, config: ConfigDict | None = None, _parent_dep
 
         core_config = config_wrapper.core_config(None)
         validator: SchemaValidator
-        if hasattr(__type, '__pydantic_validator__') and config is None:
-            validator = __type.__pydantic_validator__
-        else:
+        try:
+            validator = _getattr_no_parents(__type, '__pydantic_validator__')
+        except AttributeError:
             validator = SchemaValidator(simplified_core_schema, core_config)
 
         serializer: SchemaSerializer
-        if hasattr(__type, '__pydantic_serializer__') and config is None:
-            serializer = __type.__pydantic_serializer__
-        else:
+        try:
+            serializer = _getattr_no_parents(__type, '__pydantic_serializer__')
+        except AttributeError:
             serializer = SchemaSerializer(simplified_core_schema, core_config)
 
         self.core_schema = core_schema


Test patch:
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -22,6 +22,7 @@
     PydanticUserError,
     TypeAdapter,
     ValidationError,
+    field_serializer,
     field_validator,
 )
 from pydantic._internal._model_construction import MockValidator
@@ -2083,8 +2084,10 @@ def test_init_vars_call_monkeypatch(remove_monkeypatch, monkeypatch):
     assert stack_depth == 1 if remove_monkeypatch else 2
 
 
-def test_vanilla_dataclass_validators_in_model_field():
-    @dataclasses.dataclass
+@pytest.mark.parametrize('decorator1', **dataclass_decorators())
+@pytest.mark.parametrize('decorator2', **dataclass_decorators())
+def test_decorators_in_model_field(decorator1, decorator2):
+    @decorator1
     class Demo1:
         int1: int
 
@@ -2092,7 +2095,11 @@ class Demo1:
         def set_int_1(cls, v):
             return v + 100
 
-    @dataclasses.dataclass
+        @field_serializer('int1')
+        def serialize_int_1(self, v):
+            return v + 10
+
+    @decorator2
     class Demo2(Demo1):
         int2: int
 
@@ -2100,14 +2107,24 @@ class Demo2(Demo1):
         def set_int_2(cls, v):
             return v + 200
 
+        @field_serializer('int2')
+        def serialize_int_2(self, v):
+            return v + 20
+
     class Model(BaseModel):
         x: Demo2
 
-    assert Model.model_validate(dict(x=dict(int1=1, int2=2))).model_dump() == {'x': {'int1': 101, 'int2': 202}}
+    m = Model.model_validate(dict(x=dict(int1=1, int2=2)))
+    assert m.x.int1 == 101
+    assert m.x.int2 == 202
 
+    assert m.model_dump() == {'x': {'int1': 111, 'int2': 222}}
 
-def test_vanilla_dataclass_validators_in_type_adapter():
-    @dataclasses.dataclass
+
+@pytest.mark.parametrize('decorator1', **dataclass_decorators())
+@pytest.mark.parametrize('decorator2', **dataclass_decorators())
+def test_vanilla_dataclass_decorators_in_type_adapter(decorator1, decorator2):
+    @decorator1
     class Demo1:
         int1: int
 
@@ -2115,7 +2132,11 @@ class Demo1:
         def set_int_1(cls, v):
             return v + 100
 
-    @dataclasses.dataclass
+        @field_serializer('int1')
+        def serialize_int_1(self, v):
+            return v + 10
+
+    @decorator2
     class Demo2(Demo1):
         int2: int
 
@@ -2123,7 +2144,14 @@ class Demo2(Demo1):
         def set_int_2(cls, v):
             return v + 200
 
+        @field_serializer('int2')
+        def serialize_int_2(self, v):
+            return v + 20
+
     adapter = TypeAdapter(Demo2)
 
     m = adapter.validate_python(dict(int1=1, int2=2))
-    assert adapter.dump_python(m) == {'int1': 101, 'int2': 202}
+    assert m.int1 == 101
+    assert m.int2 == 202
+
+    assert adapter.dump_python(m) == {'int1': 111, 'int2': 222}