为响应多态数组的 HTTP API 编写 OpenAPI 模式并生成客户端代码
Writing OpenAPI schema and generating client code for HTTP API that responds with polymorphic array
我正在尝试为 HTTP API 编写 OpenAPI 3.0 架构。它的一个请求以对象的多态数组响应,如下所示:
[
{
"type": "Base",
"properties": {
"baseProp1": "Alpha",
"baseProp2": "Bravo",
"baseProp3": "Charlie"
}
},
{
"type": "Derived",
"properties": {
"baseProp1": "Delta",
"baseProp2": "Echo",
"baseProp3": "Foxtrot",
"derivedPropA": "Golf"
}
}
]
换句话说,响应可以是 Base
个对象 and/or Derived
个对象的数组,它派生自 Base
。确切的名称和属性与所讨论的实际 API 不同,但表示对象继承的约定是相同的。
我有很多与此相关的问题:
- 我注意到对象的属性包含在
"properties"
属性 中。上面的示例是 HTTP API 序列化多态数组的 JSON 兼容方式吗?
- 典型的 API 代码生成器能否正确反序列化?我使用的特定代码生成器是 Visual Studio's 2022 build-in OpenAPI 'connected service' 生成器(本质上是 NSwag)。理想情况下,我希望为
Base
和 Derived
生成的 classes 不暴露这样一个事实,即它们被反序列化的 JSON 的属性包含在 "properties"
中(即 BaseProp1
、BaseProp2
等是在 class 本身上定义的`)
- 假设我的代码生成器可以解决这个问题,是否有一种特定的方法我必须在 OpenAPI 架构中定义响应才能正确执行此操作?
最好,我想按原样使用生成的 C# 客户端代码,但如果需要,我愿意扩展它(通过部分 class)。
我承认,我可以(并且将会)围绕所涉及的技术做更多的研究,特别是客户端代码中使用的 Newtonsoft Json.NET,但我想先提交问题。
2 月 2 日更新
经过进一步调查和实验,我对解决方案进行了其他更改:
类型鉴别器
Swagger/OpenAPI 3.0 规范支持与 inheritance and polymorphism 相关的功能,即对象上的 属性 可用于区分其子类型。在 getAll
操作的情况下,这可以在架构中定义如下:
...
paths:
/getAll:
get:
operationId: getAll
responses:
'200':
description: Gets all objects
content:
application/json:
schema:
type: array
items:
oneOf:
- $ref: '#/components/schemas/BaseResponse'
- $ref: '#/components/schemas/DerivedResponse'
discriminator:
propertyName: type
mapping:
Base: '#/components/schemas/BaseResponse'
Derived: '#/components/schemas/DerivedResponse'
...
components:
schemas:
BaseResponse:
type: object
description: An item in the array type response for getAll operation
properties:
type:
$ref: '#/components/schemas/ObjectType'
properties:
$ref: '#/components/schemas/Base'
DerivedResponse:
allOf:
- $ref: '#/components/schemas/BaseResponse'
properties:
properties:
$ref: '#/components/schemas/Derived'
请注意,我已将 GetAllResponseItem
方案替换为两个新方案 BaseResponse
和 DerivedResponse
。另请参阅 GitHub 存储库中的分支 attempt3。
删除了接口 IBase
和 IDerived
并添加了 BaseResponse.Object
虚拟 属性
不需要 IBase
和 IDerived
接口,所以我删除了它们。
为了适应两种新的响应类型BaseResponse
和DerivedResponse
,我创建了一个新的虚拟get 属性 Object
类型Base
在 Derived
class 上覆盖了 return Properties
属性 的值。这是为了桥接 BaseResponse.Properties
和 DerivedResponse.Properties
,因为后者 属性 隐藏了前者,这样我们就可以访问 Properties
属性 的正确 DerivedResponse
对象,当通过 BaseResponse
类型访问其 Derived
对象时。以下代码应说明:
var derivedResponse = new DerivedResponse
{
Type = ObjectType.Derived,
Properties = new Derived()
};
BaseResponse derivedResponseViaBase = (BaseResponse)derivedResponse;
// BaseResponse.Properties is null because it's a different property to
// DerivedResponse.Properties
Assert.IsNull(derivedResponseViaBase.Properties);
// Normally, you would have to cast the `BaseResponse` back to
// `DerivedResponse` to get the correct `Derived` object
Assert.That((DerivedResponse)derivedResponseViaBase.Properties,
Is.SameAs(derivedResponse.Object))
// Instead, we can use the BaseResponse.Object which, as a virtual
// method, will always provide the correct `Base` or subtype thereof
Assert.That(derivedResponseViaBase.Object, Is.SameAs(derivedResponse.Object));
Assert.That(derivedResponseViaBase.Object, Is.TypeOf<Derived>);
另请参阅 GitHub 存储库中的分支 attempt4
原回答
经过一段时间的研究和试验,以下是我对自己问题的回答:
- I notice that the properties for the objects are wrapped in a "properties" property. Is the example above a JSON compliant way for
an HTTP API to serialize polymorphic arrays?
就对象包含类型数据的 JSON 约定而言,我努力寻找一个明确的约定 - 它似乎是 up to the developer what convention to use。
- Will a typical API code generator be able to deserialize this
correctly? The particular code generator I'm using is Visual Studio's
2022 build-in OpenAPI 'connected service' generator (essentially
NSwag). Ideally I would want the generated classes for Base and
Derived not to expose the fact that the JSON they were deserialized
from had their properties wrapped in "properties" (i.e. BaseProp1,
BaseProp2, etc. are defined on the class itself)
可能有一种方法可以指示 JSON 解析器将 { "type": [Type], "properties": { ... }
形式的对象解释为单个对象(而不是两个嵌套的对象),但是我'我还没有找到使用 NSwag 或 Newtonsoft 自动执行此操作的方法。
在 Swagger/Open API 3.0 规范中有一种方法可以做到这一点。请参阅我上面的更新,仍然需要 Inheritance and Polymorphism section of the Swagger specification. When using NSwag to generate a C# client, a JsonSubtypes 转换器,以通知 JSON 反序列化器类型关系。
- Assuming my code generator can accommodate this, is there a particular way I must define the response in the OpenAPI schema for it
to do this correctly?
是的,我找到了一种以我想要的方式执行此操作的方法。它确实涉及对生成的客户端代码的一些定制。最后我的解决方法如下
正在创建 API 架构
第一步是创建一个 OpenAPI/Swagger 定义以下内容的模式:
- 名为
Base
的模式 object
- 一个名为
Derived
的模式 object
派生自 Base
- 一个名为
GetAllResponseItem
的 object
类型的架构,它包装了 Base
个对象及其派生对象
- 名为
ObjectType
且类型为 string
的架构,它是一个具有值 Base
和 Derived
的枚举。
- 具有路径
getAll
的 get
操作 return 是 GetAllResponseItem
个对象的数组
这是用 YAML 编写的架构。
openapi: 3.0.0
info:
title: My OpenAPI/Swagger schema for Whosebug question #70791679
version: 1.0.0
paths:
/getAll:
get:
operationId: getAll
responses:
'200':
description: Gets all objects
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/GetAllResponseItem'
components:
schemas:
Base:
type: object
description: Base type
properties:
baseProp1:
type: string
example: Alpha
baseProp2:
type: string
example: Bravo
baseProp3:
type: string
example: Charlie
Derived:
type: object
description: Derived type that extends Base
allOf:
- $ref: '#/components/schemas/Base'
properties:
derivedPropA:
type: string
example: Golf
GetAllResponseItem:
type: object
description: An item in the array type response for getAll operation
properties:
type:
$ref: '#/components/schemas/ObjectType'
properties:
$ref: '#/components/schemas/Base'
ObjectType:
type: string
description: Discriminates the type of object (e.g. Base, Derived) the item is holding
enum:
- Base
- Derived
正在创建 C# 客户端
下一步是在 Visual Studio 中创建 C# 架构。我通过创建一个 C# Class 库项目和 adding an OpenAPI connected service 使用上述文件作为模式来做到这一点。这样做会生成一个代码文件,该文件定义了以下部分 classes:
MyApiClient
Base
Derived
(继承Base
)
GetAllResponseItem
(Type
属性 类型 ObjectType
和 Properties
属性 类型 Base
)
ObjectType
(包含项目 Base
和 Derived
的枚举)
ApiException
(对于本次讨论不重要)
接下来我安装了 JsonSubtypes nuget 包。这将允许我们在 API 客户端中指示 JSON 反序列化器,当它期望 Base
对象时,在 JSON 有 DerivedPropA
属性.
接下来,我添加以下代码文件来扩展生成的 API 代码:
MyApiClient.cs:
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JsonSubTypes;
using Newtonsoft.Json;
namespace MyApi
{
public interface IBase
{
string BaseProp1 { get; set; }
string BaseProp2 { get; set; }
string BaseProp3 { get; set; }
}
public interface IDerived : IBase
{
string DerivedPropA { get; set; }
}
public interface IMyApiClient
{
Task<ICollection<IBase>> GetAllAsync(CancellationToken cancellationToken = default);
}
// Use a JsonConverter provided by JsonSubtypes, which deserializes a Base object as a Derived
// subtype when it contains a property named 'DerivedPropA'
[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Derived), nameof(Derived.DerivedPropA))]
public partial class Base : IBase {}
public partial class Derived : IDerived {}
public partial class MyApiClient : IMyApiClient
{
async Task<ICollection<IBase>> IMyApiClient.GetAllAsync(CancellationToken cancellationToken)
{
var resp = await GetAllAsync(cancellationToken).ConfigureAwait(false);
return resp.Select(o => (IBase) o.Properties).ToList();
}
}
}
接口 IBase
、IDerived
和 IMyApiClient
试图向 IMyApiClient
的消费者隐藏 API 的实际响应使用的事实输入 ICollection<GetAllResponseItem>
并提供类型 ICollection<IBase>
。这并不完美,因为没有任何东西强制使用 IMyApiClient
并且 GetAllResponseItem
class 被声明为 public。可以进一步封装它,但它可能涉及自定义客户端代码生成。
最后,这里有一些测试代码来演示用法:
Tests.cs:
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using MyApi;
using NUnit.Framework;
namespace ApiClientTests
{
public class Tests
{
private readonly IBase[] _allObjects = {
new Base {
BaseProp1 = "Alpha", BaseProp2 = "Bravo", BaseProp3 = "Charlie"
},
new Derived {
BaseProp1 = "Delta", BaseProp2 = "Echo", BaseProp3 = "Foxtrot",
DerivedPropA = "Golf"
}
};
[Test]
public void ShouldBeAbleToAccessPropertiesOnBaseAndDerivedTypes()
{
IBase baseObject = _allObjects[0];
Assert.That(baseObject, Is.TypeOf<Base>());
Assert.That(baseObject.BaseProp1, Is.EqualTo("Alpha"));
IDerived derivedObject = (IDerived)_allObjects[1];
Assert.That(derivedObject, Is.TypeOf<Derived>());
Assert.That(derivedObject.DerivedPropA, Is.EqualTo("Golf"));
}
[Test]
public void ShouldBeAbleToDiscriminateDerivativeTypesUsingTypeCasting()
{
IDerived[] derivatives = _allObjects.OfType<IDerived>().ToArray();
Assert.That(derivatives.Length, Is.EqualTo(1));
Assert.That(derivatives[0], Is.SameAs(_allObjects[1]));
}
[Ignore("Example usage only - API host doesn't exist")]
[Test]
public async Task TestGetAllOperation()
{
using var httpClient = new HttpClient();
IMyApiClient apiClient =
new MyApiClient("https://example.io/", httpClient);
var resp = await apiClient.GetAllAsync();
Assert.That(resp, Is.TypeOf<ICollection<IBase>>());
IBase[] allObjects = resp.ToArray();
Assert.That(allObjects.Length, Is.EqualTo(2));
Assert.That(allObjects[0].BaseProp1, Is.EqualTo("Alpha"));
Assert.That(((IDerived)allObjects[1]).DerivedPropA, Is.EqualTo("Golf"));
}
}
}
源代码可在 GitHub: https://github.com/DanStevens/Whosebug70791679
我很欣赏这可能是一个相当小众的问题和答案,但写下这个问题确实帮助我找到了最简单的解决方案(确实是 first attempt was more complex than my second)。也许这个问题对其他人有用。
最后,提出这个问题的实际项目也可以在 GitHub 上找到:https://github.com/DanStevens/BabelNetApiClient
我正在尝试为 HTTP API 编写 OpenAPI 3.0 架构。它的一个请求以对象的多态数组响应,如下所示:
[
{
"type": "Base",
"properties": {
"baseProp1": "Alpha",
"baseProp2": "Bravo",
"baseProp3": "Charlie"
}
},
{
"type": "Derived",
"properties": {
"baseProp1": "Delta",
"baseProp2": "Echo",
"baseProp3": "Foxtrot",
"derivedPropA": "Golf"
}
}
]
换句话说,响应可以是 Base
个对象 and/or Derived
个对象的数组,它派生自 Base
。确切的名称和属性与所讨论的实际 API 不同,但表示对象继承的约定是相同的。
我有很多与此相关的问题:
- 我注意到对象的属性包含在
"properties"
属性 中。上面的示例是 HTTP API 序列化多态数组的 JSON 兼容方式吗? - 典型的 API 代码生成器能否正确反序列化?我使用的特定代码生成器是 Visual Studio's 2022 build-in OpenAPI 'connected service' 生成器(本质上是 NSwag)。理想情况下,我希望为
Base
和Derived
生成的 classes 不暴露这样一个事实,即它们被反序列化的 JSON 的属性包含在"properties"
中(即BaseProp1
、BaseProp2
等是在 class 本身上定义的`) - 假设我的代码生成器可以解决这个问题,是否有一种特定的方法我必须在 OpenAPI 架构中定义响应才能正确执行此操作?
最好,我想按原样使用生成的 C# 客户端代码,但如果需要,我愿意扩展它(通过部分 class)。
我承认,我可以(并且将会)围绕所涉及的技术做更多的研究,特别是客户端代码中使用的 Newtonsoft Json.NET,但我想先提交问题。
2 月 2 日更新
经过进一步调查和实验,我对解决方案进行了其他更改:
类型鉴别器
Swagger/OpenAPI 3.0 规范支持与 inheritance and polymorphism 相关的功能,即对象上的 属性 可用于区分其子类型。在 getAll
操作的情况下,这可以在架构中定义如下:
...
paths:
/getAll:
get:
operationId: getAll
responses:
'200':
description: Gets all objects
content:
application/json:
schema:
type: array
items:
oneOf:
- $ref: '#/components/schemas/BaseResponse'
- $ref: '#/components/schemas/DerivedResponse'
discriminator:
propertyName: type
mapping:
Base: '#/components/schemas/BaseResponse'
Derived: '#/components/schemas/DerivedResponse'
...
components:
schemas:
BaseResponse:
type: object
description: An item in the array type response for getAll operation
properties:
type:
$ref: '#/components/schemas/ObjectType'
properties:
$ref: '#/components/schemas/Base'
DerivedResponse:
allOf:
- $ref: '#/components/schemas/BaseResponse'
properties:
properties:
$ref: '#/components/schemas/Derived'
请注意,我已将 GetAllResponseItem
方案替换为两个新方案 BaseResponse
和 DerivedResponse
。另请参阅 GitHub 存储库中的分支 attempt3。
删除了接口 IBase
和 IDerived
并添加了 BaseResponse.Object
虚拟 属性
不需要 IBase
和 IDerived
接口,所以我删除了它们。
为了适应两种新的响应类型BaseResponse
和DerivedResponse
,我创建了一个新的虚拟get 属性 Object
类型Base
在 Derived
class 上覆盖了 return Properties
属性 的值。这是为了桥接 BaseResponse.Properties
和 DerivedResponse.Properties
,因为后者 属性 隐藏了前者,这样我们就可以访问 Properties
属性 的正确 DerivedResponse
对象,当通过 BaseResponse
类型访问其 Derived
对象时。以下代码应说明:
var derivedResponse = new DerivedResponse
{
Type = ObjectType.Derived,
Properties = new Derived()
};
BaseResponse derivedResponseViaBase = (BaseResponse)derivedResponse;
// BaseResponse.Properties is null because it's a different property to
// DerivedResponse.Properties
Assert.IsNull(derivedResponseViaBase.Properties);
// Normally, you would have to cast the `BaseResponse` back to
// `DerivedResponse` to get the correct `Derived` object
Assert.That((DerivedResponse)derivedResponseViaBase.Properties,
Is.SameAs(derivedResponse.Object))
// Instead, we can use the BaseResponse.Object which, as a virtual
// method, will always provide the correct `Base` or subtype thereof
Assert.That(derivedResponseViaBase.Object, Is.SameAs(derivedResponse.Object));
Assert.That(derivedResponseViaBase.Object, Is.TypeOf<Derived>);
另请参阅 GitHub 存储库中的分支 attempt4
原回答
经过一段时间的研究和试验,以下是我对自己问题的回答:
- I notice that the properties for the objects are wrapped in a "properties" property. Is the example above a JSON compliant way for an HTTP API to serialize polymorphic arrays?
就对象包含类型数据的 JSON 约定而言,我努力寻找一个明确的约定 - 它似乎是 up to the developer what convention to use。
- Will a typical API code generator be able to deserialize this correctly? The particular code generator I'm using is Visual Studio's 2022 build-in OpenAPI 'connected service' generator (essentially NSwag). Ideally I would want the generated classes for Base and Derived not to expose the fact that the JSON they were deserialized from had their properties wrapped in "properties" (i.e. BaseProp1, BaseProp2, etc. are defined on the class itself)
可能有一种方法可以指示 JSON 解析器将 { "type": [Type], "properties": { ... }
形式的对象解释为单个对象(而不是两个嵌套的对象),但是我'我还没有找到使用 NSwag 或 Newtonsoft 自动执行此操作的方法。
在 Swagger/Open API 3.0 规范中有一种方法可以做到这一点。请参阅我上面的更新,仍然需要 Inheritance and Polymorphism section of the Swagger specification. When using NSwag to generate a C# client, a JsonSubtypes 转换器,以通知 JSON 反序列化器类型关系。
- Assuming my code generator can accommodate this, is there a particular way I must define the response in the OpenAPI schema for it to do this correctly?
是的,我找到了一种以我想要的方式执行此操作的方法。它确实涉及对生成的客户端代码的一些定制。最后我的解决方法如下
正在创建 API 架构
第一步是创建一个 OpenAPI/Swagger 定义以下内容的模式:
- 名为
Base
的模式object
- 一个名为
Derived
的模式object
派生自Base
- 一个名为
GetAllResponseItem
的object
类型的架构,它包装了Base
个对象及其派生对象 - 名为
ObjectType
且类型为string
的架构,它是一个具有值Base
和Derived
的枚举。 - 具有路径
getAll
的get
操作 return 是GetAllResponseItem
个对象的数组
这是用 YAML 编写的架构。
openapi: 3.0.0
info:
title: My OpenAPI/Swagger schema for Whosebug question #70791679
version: 1.0.0
paths:
/getAll:
get:
operationId: getAll
responses:
'200':
description: Gets all objects
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/GetAllResponseItem'
components:
schemas:
Base:
type: object
description: Base type
properties:
baseProp1:
type: string
example: Alpha
baseProp2:
type: string
example: Bravo
baseProp3:
type: string
example: Charlie
Derived:
type: object
description: Derived type that extends Base
allOf:
- $ref: '#/components/schemas/Base'
properties:
derivedPropA:
type: string
example: Golf
GetAllResponseItem:
type: object
description: An item in the array type response for getAll operation
properties:
type:
$ref: '#/components/schemas/ObjectType'
properties:
$ref: '#/components/schemas/Base'
ObjectType:
type: string
description: Discriminates the type of object (e.g. Base, Derived) the item is holding
enum:
- Base
- Derived
正在创建 C# 客户端
下一步是在 Visual Studio 中创建 C# 架构。我通过创建一个 C# Class 库项目和 adding an OpenAPI connected service 使用上述文件作为模式来做到这一点。这样做会生成一个代码文件,该文件定义了以下部分 classes:
MyApiClient
Base
Derived
(继承Base
)GetAllResponseItem
(Type
属性 类型ObjectType
和Properties
属性 类型Base
)ObjectType
(包含项目Base
和Derived
的枚举)ApiException
(对于本次讨论不重要)
接下来我安装了 JsonSubtypes nuget 包。这将允许我们在 API 客户端中指示 JSON 反序列化器,当它期望 Base
对象时,在 JSON 有 DerivedPropA
属性.
接下来,我添加以下代码文件来扩展生成的 API 代码:
MyApiClient.cs:
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JsonSubTypes;
using Newtonsoft.Json;
namespace MyApi
{
public interface IBase
{
string BaseProp1 { get; set; }
string BaseProp2 { get; set; }
string BaseProp3 { get; set; }
}
public interface IDerived : IBase
{
string DerivedPropA { get; set; }
}
public interface IMyApiClient
{
Task<ICollection<IBase>> GetAllAsync(CancellationToken cancellationToken = default);
}
// Use a JsonConverter provided by JsonSubtypes, which deserializes a Base object as a Derived
// subtype when it contains a property named 'DerivedPropA'
[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Derived), nameof(Derived.DerivedPropA))]
public partial class Base : IBase {}
public partial class Derived : IDerived {}
public partial class MyApiClient : IMyApiClient
{
async Task<ICollection<IBase>> IMyApiClient.GetAllAsync(CancellationToken cancellationToken)
{
var resp = await GetAllAsync(cancellationToken).ConfigureAwait(false);
return resp.Select(o => (IBase) o.Properties).ToList();
}
}
}
接口 IBase
、IDerived
和 IMyApiClient
试图向 IMyApiClient
的消费者隐藏 API 的实际响应使用的事实输入 ICollection<GetAllResponseItem>
并提供类型 ICollection<IBase>
。这并不完美,因为没有任何东西强制使用 IMyApiClient
并且 GetAllResponseItem
class 被声明为 public。可以进一步封装它,但它可能涉及自定义客户端代码生成。
最后,这里有一些测试代码来演示用法:
Tests.cs:
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using MyApi;
using NUnit.Framework;
namespace ApiClientTests
{
public class Tests
{
private readonly IBase[] _allObjects = {
new Base {
BaseProp1 = "Alpha", BaseProp2 = "Bravo", BaseProp3 = "Charlie"
},
new Derived {
BaseProp1 = "Delta", BaseProp2 = "Echo", BaseProp3 = "Foxtrot",
DerivedPropA = "Golf"
}
};
[Test]
public void ShouldBeAbleToAccessPropertiesOnBaseAndDerivedTypes()
{
IBase baseObject = _allObjects[0];
Assert.That(baseObject, Is.TypeOf<Base>());
Assert.That(baseObject.BaseProp1, Is.EqualTo("Alpha"));
IDerived derivedObject = (IDerived)_allObjects[1];
Assert.That(derivedObject, Is.TypeOf<Derived>());
Assert.That(derivedObject.DerivedPropA, Is.EqualTo("Golf"));
}
[Test]
public void ShouldBeAbleToDiscriminateDerivativeTypesUsingTypeCasting()
{
IDerived[] derivatives = _allObjects.OfType<IDerived>().ToArray();
Assert.That(derivatives.Length, Is.EqualTo(1));
Assert.That(derivatives[0], Is.SameAs(_allObjects[1]));
}
[Ignore("Example usage only - API host doesn't exist")]
[Test]
public async Task TestGetAllOperation()
{
using var httpClient = new HttpClient();
IMyApiClient apiClient =
new MyApiClient("https://example.io/", httpClient);
var resp = await apiClient.GetAllAsync();
Assert.That(resp, Is.TypeOf<ICollection<IBase>>());
IBase[] allObjects = resp.ToArray();
Assert.That(allObjects.Length, Is.EqualTo(2));
Assert.That(allObjects[0].BaseProp1, Is.EqualTo("Alpha"));
Assert.That(((IDerived)allObjects[1]).DerivedPropA, Is.EqualTo("Golf"));
}
}
}
源代码可在 GitHub: https://github.com/DanStevens/Whosebug70791679
我很欣赏这可能是一个相当小众的问题和答案,但写下这个问题确实帮助我找到了最简单的解决方案(确实是 first attempt was more complex than my second)。也许这个问题对其他人有用。
最后,提出这个问题的实际项目也可以在 GitHub 上找到:https://github.com/DanStevens/BabelNetApiClient