简体   繁体   English

使用 Pydantic 使用可选元素进行 API JSON 模式验证

[英]API JSON Schema Validation with Optional Element using Pydantic

I am using fastapi and BaseModel from pydantic to validate and document the JSON schema for an API return.我正在使用 pydantic 的 fastapi 和 BaseModel 来验证和记录 API 返回的 JSON 模式。

This works well for a fixed return but I have optional parameters that change the return so I would like to include it in the validation but for it not to fail when the parameter is missing and the field is not returned in the API.这适用于固定返回值,但我有可选参数可以更改返回值,因此我想将其包含在验证中,但在缺少参数且 API 中未返回该字段时不会失败。

For example: I have an optional boolean parameter called transparency when this is set to true I return a block called search_transparency with the elastic query returned.例如:当它设置为 true 时,我有一个名为transparency的可选布尔参数,我返回一个名为 search_transparency 的块,并返回弹性查询。

{
  "info": {
    "totalrecords": 52
  },
  "records": [],
  "search_transparency": {"full_query": "blah blah"}
}

If the parameter transparency=true is not set I want the return to be:如果未设置参数transparency=true我希望返回是:

{
  "info": {
    "totalrecords": 52
  },
  "records": []
}

However, when I set that element to be Optional in pydantic, I get this returned instead:但是,当我在 pydantic 中将该元素设置为 Optional 时,我反而返回了它:

{
  "info": {
    "totalrecords": 52
  },
  "records": [],
  "search_transparency": None
}

I have something similar for the fields under records.对于记录下的字段,我有类似的东西。 The default is a minimal return of fields but if you set the parameter full=true then you get many more fields returned.默认值是最小的字段返回,但如果您设置参数full=true那么您将返回更多的字段。 I would like to handle this in a similar way with the fields just being absent rather than shown with a value of None .我想以类似的方式处理这个问题,字段只是不存在,而不是显示为None值。

This is how I am handling it with pydantic:这就是我用 pydantic 处理它的方式:

class Info(BaseModel):
    totalrecords: int

class Transparency(BaseModel):
    full_query: str

class V1Place(BaseModel):
    name: str

class V1PlaceAPI(BaseModel):
    info: Info
    records: List[V1Place] = []
    search_transparency: Optional[Transparency]

and this is how I am enforcing the validation with fastapi:这就是我使用 fastapi 强制验证的方式:

@app.get("/api/v1/place/search", response_model=V1PlaceAPI, tags=["v1_api"])

I have a suspicion that maybe what I am trying to achieve is poor API practice, maybe I am not supposed to have variable returns.我怀疑也许我想要实现的是糟糕的 API 实践,也许我不应该有可变回报。

Should I instead be creating multiple separate endpoints to handle this?我应该创建多个单独的端点来处理这个问题吗?

eg.例如。 api/v1/place/search?q=test vs api/v1/place/full/transparent/search?q=test api/v1/place/search?q=test vs api/v1/place/full/transparent/search?q=test

EDIT编辑

More detail of my API function:我的 API 函数的更多细节:

@app.get("/api/v1/place/search", response_model=V1PlaceAPI, tags=["v1_api"])

def v1_place_search(q: str = Query(None, min_length=3, max_length=500, title="search through all place fields"),
                    transparency: Optional[bool] = None,
                    offset: Optional[int] = Query(0),
                    limit: Optional[int] = Query(15)):

    search_limit = offset + limit

    results, transparency_query = ESQuery(client=es_client,
                                          index='places',
                                          transparency=transparency,
                                          track_hits=True,
                                          offset=offset,
                                          limit=search_limit)

    return v1_place_parse(results.to_dict(), 
    show_transparency=transparency_query)

where ESQuery just returns an elasticsearch response.其中 ESQuery 只返回一个 elasticsearch 响应。 And this is my parse function:这是我的解析函数:

def v1_place_parse(resp, show_transparency=None):
    """This takes a response from elasticsearch and parses it for our legacy V1 elasticapi

    Args:
        resp (dict): This is the response from Search.execute after passing to_dict()

    Returns:
        dict: A dictionary that is passed to API
    """

    new_resp = {}
    total_records = resp['hits']['total']['value']
    query_records = len(resp.get('hits', {}).get('hits', []))

    new_resp['info'] = {'totalrecords': total_records,
                        'totalrecords_relation': resp['hits']['total']['relation'],
                        'totalrecordsperquery': query_records,
                        }
    if show_transparency is not None:
        search_string = show_transparency.get('query', '')
        new_resp['search_transparency'] = {'full_query': str(search_string),
                                           'components': {}}
    new_resp['records'] = []
    for hit in resp.get('hits', {}).get('hits', []):
        new_record = hit['_source']
        new_resp['records'].append(new_record)

    return new_resp

Probably excluding that field if it is None can get the job done.可能不包括该字段,如果它是None可以完成工作。

Just add a response_model_exclude_none = True as a path parameter只需添加response_model_exclude_none = True作为路径参数

@app.get(
    "/api/v1/place/search",
    response_model=V1PlaceAPI,
    tags=["v1_api"],
    response_model_exclude_none=True,
)

You can customize your Response model even more, here is a well explained answer of mine I really suggest you check it out.您可以更多地自定义您的响应模型,这是我的一个很好解释的答案,我真的建议您检查一下。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM