Previously we supported encoding dataclasses (determined as any object with a `__dataclass_fields__` attribute), provided those objects were implemented in a way similar-enough to how they were implemented in the standard library. The intent was to support stdlib dataclasses, and if alternative implementations (e.g. `pydantic.dataclasses`) happened to work then all the better. However, due to how we were detecting if an object was a dataclass, there was no way to override our builtin support if an alternative implementation (in this case `edgedb.Object`) didn't work. To fix this, we now make fewer assumptions about how the backing dataclass object is implemented. Pros: - We can now natively encode objects implemented using `dataclasses`, `pydantic.dataclasses`, and `edgedb.Object`. - We now only encode fields as declared on the dataclass object. Previously we encoded any attribute lacking a leading underscore, which was efficient and worked well in practice (it's also what `orjson` does). However, this can lead to weird behavior if some fields intentionally start with an `_` (like `_id` in mongodb) or if the object makes use of `functools.cached_property`. Cons: - This flexibility and correctness comes at a performance cost. The fast path is the common case (dataclass uses `__dict__`, doesn't override `__getattribute__`), but encoding is now ~20% slower than before. Before we encoded `__dict__` based dataclasses 20% faster than orjson; now we're faster for small classes (<= 8 items, on my machine) and slower for larger numbers of fields. For `__slots__` based classes we're still around 2x faster than `orjson`.