Question: Consider a third-party class that doesn't support pydantic serialization, and you're not under control of the source code of that class, i.e., you cannot make it inherit from BaseModel. Let's assume the entire state of that class can be constructed and obtained via its public interface (but the class may have private fields). So in theory we'd be able to write serialization/deserialization functions of that class based on its public interface. Is it somehow possible to use such a class inside a pydantic BaseModel? I.e., the goal would be to somehow arrive at: class MySerializableClass(BaseModel): foreign_class_instance: ForeignClass  How could we add serialization/deserialization functions to properly support the foreign_class_instance field?  As a concrete let's take for instance a Numpy array as an example of a foreign class that we want to support. By serializing/deserializing based on the "public interface" I mean:  For serialization we can use the public interface of np.array to get its data including meta data like dtype and shape. The output of the serialization function could be something like {"dtype": "int", "shape": [3], "data": [1, 2, 3]} (or any other composition of JSON-serializable data). The serialization function would have a signature like serialize(x: np.ndarray) -> object (for lack of the JsonData type). The deserialization function would get this serialized representation, and would construct the np.ndarray instance again on the public interface of np.ndarray, which typically means using its constructor. The signature would be the inverse: deserialize(o: object) -> np.ndarray.  My question is: Assuming I can implement these two serialize and deserialize functions just fine like in this example, how can I integrate them into pydantic so that serializing/deserializing a BaseModel implicitly makes use of the two functions.

Answer: In general, the process of defining custom field types is described in this section of the documentation. The bare minimum is a method called __get_validators__ returning an iterator of validation methods, the last of which should return an instance of that custom type. That means with third-party classes you will almost certainly need to subclass (or monkey-patch) them. If the type you are dealing with has no built-in JSON serialization support (types like str or list for example are supported out of the box), you also need to sepcify how instances of that type are supposed to be dumped to JSON. This can be done selectively, when calling the json method or configured model-wide via the json_encoders dictionary. The example of the Numpy ndarray is slightly more involved because we need to  define a sensible basic constructor for our subclass and decide what type of data we want to support during parsing/validation.  A very simple setup might look like this: from __future__ import annotations from collections.abc import Callable, Iterator, Mapping from typing import Any import numpy as np from numpy.typing import ArrayLike from pydantic import BaseModel class SerializableNDArray(np.ndarray[Any, Any]): def __new__(cls, array_like: ArrayLike) -> SerializableNDArray: return np.asarray(array_like).view(cls) @classmethod def __get_validators__(cls) -> Iterator[Callable[..., Any]]: yield cls.validate @classmethod def validate(cls, v: Any) -> SerializableNDArray: if isinstance(v, Mapping): return cls(np.array(**v)) return cls(v) def json_dump(self) -> dict[str, Any]: return {"object": self.tolist(), "dtype": str(self.dtype)} class Model(BaseModel): a: SerializableNDArray class Config: json_encoders = { SerializableNDArray: SerializableNDArray.json_dump }  Here is a little demo: def main() -> None: test_json_1 = '{"a": [1.0, 2.0, 3.0]}' test_json_2 = '{"a": {"object": [[1, 2], [3, 4]], "dtype": "float32"}}' obj1 = Model.parse_raw(test_json_1) obj2 = Model.parse_raw(test_json_2) print(obj1.json(indent=4)) print(obj2.json(indent=4)) if __name__ == "__main__": main()  Output: { "a": { "object": [ 1.0, 2.0, 3.0 ], "dtype": "float64" } }  { "a": { "object": [ [ 1.0, 2.0 ], [ 3.0, 4.0 ] ], "dtype": "float32" } }  Notice that I decided to make the JSON representation of our custom array type always be an object with the keys object and dtype instead of simply an array of numbers because this allows us to preserve more information (in this example the data type) and the validator accepts a mapping that can be unpacked into the numpy.array function. (So those keys need to be a subset of the parameter names of that function.) I left out the shape deliberately here because it is not a parameter of numpy.array. The shape is inferred from the way the "array-like" object passed to that function is structured (list of lists for example). If the data to validate is not some kind of mapping, the validator will always to try to coerce the value directly via our constructor, i.e. via numpy.asarray. That should give type errors that are nice enough, when invalid data is parsed. Obviously you could design this very differently. You could use a different constructor or a totally different validator that allows you to e.g. pass data that can be unpacked into the numpy.arange function instead (there you could use shape for example). Or you could decide that dumping it simply as an array of numbers is sufficient for your needs. This is just an example, but I think it illustrates the approach.