如何将 Pydantic 模型实例导出为 YAML,URL 类型为字符串

问题描述 投票:0回答:2

我有一个 Pydantic 模型,其字段类型为

AnyUrl
。 将模型导出到 YAML 时,
AnyUrl
被序列化为单独的字段槽,而不是单个字符串 URL(可能是由于
AnyUrl.__repr__
方法的实现方式)。

例如:

from pydantic import BaseModel, AnyUrl
import yaml

class MyModel(BaseModel):
    url: AnyUrl


data = {'url': 'https://www.example.com'}
model = MyModel.parse_obj(data)

y = yaml.dump(model.dict(), indent=4)
print(y)

产品:

url: !!python/object/new:pydantic.networks.AnyUrl
    args:
    - https://www.example.com
    state: !!python/tuple
    - null
    -   fragment: null
        host: www.example.com
        host_type: domain
        password: null
        path: null
        port: null
        query: null
        scheme: https
        tld: com
        user: null

理想情况下,我希望序列化的 YAML 包含

https://www.example.com
而不是单个字段。

我尝试重写

__repr__
AnyUrl
方法来返回
AnyUrl
对象本身,因为它扩展了
str
类,但没有成功。

python yaml pydantic pyyaml
2个回答
3
投票

不幸的是,

pyyaml
文档实在是太可怕了,因此像定制(反)序列化这样看似基本的事情很难正确理解。但本质上有两种方法可以解决这个问题。

选项A:子类
YAMLObject

您对子类化

AnyUrl
的想法是正确的,但是
__repr__
方法与 YAML 序列化无关。为此,您需要做三件事:

  1. 继承自
    YAMLObject
    ,
  2. 定义自定义
    yaml_tag
    ,并且
  3. 覆盖
    to_yaml
    类方法。

然后

pyyaml
将根据您在
AnyUrl
中定义的内容序列化此自定义类(继承自
YAMLObject
to_yaml
)。

to_yaml
方法总是接收两个参数:

  1. 一个
    yaml.Dumper
    实例,具有序列化标准类型的内置功能(例如通过
    represent_str
    等方法)和
  2. 要序列化的实际数据。

为了避免添加/覆盖其他方法,您可以利用

AnyUrl
继承自 string 的事实,并且底层
str.__new__
方法实际上在构造过程中接收 full URL。因此
str.__str__
方法将返回“原样”。

from pydantic import AnyUrl, BaseModel
from yaml import Dumper, ScalarNode, YAMLObject, dump, safe_load


class Url(AnyUrl, YAMLObject):
    yaml_tag = "!Url"

    @classmethod
    def to_yaml(cls, dumper: Dumper, data: str) -> ScalarNode:
        return dumper.represent_str(str.__str__(data))


class MyModel(BaseModel):
    foo: int = 0
    url: Url


obj = MyModel.parse_obj({"url": "https://www.example.com"})
print(obj)

serialized = dump(obj.dict()).strip()
print(serialized)

deserialized = MyModel.parse_obj(safe_load(serialized))
print(deserialized == obj and isinstance(deserialized.url, Url))

输出:

foo=0 url=Url('https://www.example.com', scheme='https', host='www.example.com', tld='com', host_type='domain')
foo: 0
url: https://www.example.com
True

选项 B:为 AnyUrl 注册一个
representer

函数

您可以避免定义自己的子类,而是通过使用 AnyUrl 函数

全局
注册一个定义如何序列化
yaml.add_representer
实例的函数。

该函数需要两个强制参数:

  1. 您要为其定义自定义序列化行为的类以及
  2. 定义序列化行为的representer函数。

表示器函数本质上必须具有与选项 A 中提供的

YAMLObject.to_yaml
类方法相同的签名,即它采用
Dumper
实例和要序列化的数据作为参数。

from pydantic import AnyUrl, BaseModel
from yaml import Dumper, ScalarNode, add_representer, dump, safe_load


def url_representer(dumper: Dumper, data: AnyUrl) -> ScalarNode:
    return dumper.represent_str(str.__str__(data))


add_representer(AnyUrl, url_representer)


class MyModel(BaseModel):
    foo: int = 0
    url: AnyUrl


obj = MyModel.parse_obj({"url": "https://www.example.com"})
print(obj)

serialized = dump(obj.dict()).strip()
print(serialized)

deserialized = MyModel.parse_obj(safe_load(serialized))
print(deserialized == obj and isinstance(deserialized.url, AnyUrl))

输出与选项 A 中的代码相同。

这种方法的好处是它涉及较少的代码以及选项 A 中两个父类之间潜在的命名空间冲突。

一个潜在的缺点是,它会修改程序整个运行时的 global 设置,如果您的应用程序变得很大并且只是需要注意,以防您决定要序列化,这可能会变得不太透明

 AnyUrl
在某些时候会有不同的对象。


0
投票

由于其他一些用例,我遇到了这个线程。现在 pydantic 似乎已经通过使用

field_serializer
解决了这个问题。我自己还没有测试过,但它应该像这样工作:

from pydantic import BaseModel, field_serializer

class MyModel(BaseModel):
    url: AnyUrl
    
    @field_serializer("url")
    def serialize_url(self, url: AnyUrl, _info):
        return str(url)
© www.soinside.com 2019 - 2024. All rights reserved.