在 Swift 中轻松进行 JSON:API 编码和解码。
使用 Swift 编码和解码 JSON:API 响应可能会带来一些挑战。以下是一些常见问题:
复杂和嵌套的结构
JSON:API 响应通常具有深度嵌套的结构,包括 attributes
、relationships
和其他需要仔细映射到 Swift 类型的 sections。
包含的资源
JSON:API 响应中的 included
部分包含相关的资源,我们必须解析这些资源并将其链接到主要数据。在代码中管理这些关系需要仔细关注细节。
多态关系
JSON:API 支持多态关系,其中相关的资源可以是不同的类型。解码这些关系涉及处理给定关系的多种可能类型。
JSON:API 规范中一个反复出现的例子是包含文章列表的响应。每篇文章都有一位作者和零个或多个评论。每条评论可以选择性地有一位作者。
我们可以将这些资源建模为简单的结构体,并使用 @ResourceWrapper
宏来启用 JSON:API 编码和解码。
要定义资源的属性和关系,我们分别使用用 @ResourceAttribute
和 @ResourceRelationship
注释的属性。
@ResourceWrapper(type: "people")
struct Person: Equatable {
var id: String
@ResourceAttribute var firstName: String
@ResourceAttribute var lastName: String
@ResourceAttribute var twitter: String?
}
@ResourceWrapper(type: "comments")
struct Comment: Equatable {
var id: String
@ResourceAttribute var body: String
@ResourceRelationship var author: Person?
}
@ResourceWrapper(type: "articles")
struct Article: Equatable {
var id: String
@ResourceAttribute var title: String
@ResourceRelationship var author: Person
@ResourceRelationship var comments: [Comment]
}
要从 JSON:API 响应中解码 Article
值的数组,我们必须使用 JSONAPIDecoder
对象。JSONAPIDecoder
是一个 JSONDecoder
子类,它允许解码并将响应中任何 included
的相关资源嵌入其中。
let decoder = JSONAPIDecoder()
let articles = try decoder.decode([Article].self, from: json)
同样,我们可以使用 JSONAPIEncoder
对象将文章数组编码回 JSON:API 响应。与它的解码对应物一样,JSONAPIEncoder
允许将相关资源编码到 included
数组中。
let encoder = JSONAPIEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(articles)
重要提示
我们建议不要修改用 @ResourceWrapper
注释的模型中的关系,因为维护跨重复实例的一致性可能具有挑战性。
当向 JSON:API 后端发送创建或更新请求时,您不需要提供所有属性或关系。 同样,在更新关系时,您只需要提供相关资源标识符,而无需提供整个相关资源。
@ResourceWrapper
宏生成便利方法来构建创建或更新请求的主体。
以下示例显示了如何获取创建新文章的请求的主体
let newArticle = Article.createBody(
title: "A guide to parsing JSON:API with Swift",
author: "9"
)
// You can use a regular `JSONEncoder` to encode the request body
let encoder = JSONEncoder()
let data = try encoder.encode(newArticle)
请注意,您只需要提供作者关系的标识符。为了方便起见和类型安全,关系参数类型编码了相关资源类型字符串(在本例中为 "people"
)。
以下是另一个示例,显示了如何获取将一些评论添加到现有文章的请求的主体。
let articleUpdate = Article.updateBody(id: "1", comments: ["5", "12"])
多态关系是一种资源可以与多种类型的资源相关的关系。这意味着单个关系字段可以引用不同的资源类型。
例如,Article
可能具有可以指向 Person
或 Organization
资源的 "contributors"
关系。
"relationships": {
"contributors": {
"data": [
{
"type": "people",
"id": "12"
},
{
"type": "organizations",
"id": "25"
}
]
}
}
我们可以像处理其他资源一样,利用 @ResourceWrapper
宏来建模 Organization
资源。
@ResourceWrapper(type: "organizations")
struct Organization: Equatable {
var id: String
@ResourceAttribute var name: String
@ResourceAttribute var contactEmail: String
}
对于 "contributors"
关系,我们必须创建一个组合 Person
和 Organization
类型的类型。我们可以使用具有关联值的 enum
类型来实现这一点,然后使用 @ResourceUnion
宏对其进行注释。
@ResourceUnion
enum Contributor: Equatable {
case person(Person)
case organization(Organization)
}
完成之后,我们可以将 contributors
关系添加到 Article
。
@ResourceWrapper(type: "articles")
struct Article: Equatable {
var id: String
@ResourceAttribute var title: String
@ResourceRelationship var author: Person
@ResourceRelationship var comments: [Comment]
// Both people and organizations can contribute to an article
@ResourceRelationship var contributors: [Contributor]
}
除其他事项外,@ResourceUnion
宏会生成一个 ID
类型,您可以使用该类型来标识参与 union 的资源。例如,以下是如何构建请求以更新现有文章的贡献者的主体
let articleUpdate = Article.updateBody(
id: "1",
contributors: [
.person("12"),
.organization("24"),
.person("66")
]
)
一些 JSON:API 响应可能包含顶层元信息,以提供不适合主要数据的其他详细信息,例如请求标识符或分页元数据。
{
"meta": {
"requestId": "abcd-1234",
"pagination": {
"totalPages": 10,
"currentPage": 2
},
},
"data": [
{
"type": "articles",
"id": "1",
...
},
...
]
}
要从 JSON:API 响应中获取顶层元信息,您必须提供合适的 Codable
模型,并将其与 CompoundDocument
类型一起使用。
struct Meta: Equatable, Codable {
struct Pagination: Equatable, Codable {
let totalPages: Int
let currentPage: Int
}
let requestId: String
let pagination: Pagination
}
typealias ArticlesDocument = CompoundDocument<[Article], Meta>
let decoder = JSONAPIDecoder()
let document = try decoder.decode(ArticlesDocument.self, from: json)
let currentPage = document.meta.pagination.currentPage
let articles = document.data
在解码 JSON API 响应时,有些情况下您需要灵活性,并且希望获得不完整的响应而不是错误。
如果解码器无法在 included
部分中找到关系引用的资源,则会抛出 DecodingError.valueNotFound
错误。
我们可以通过使用可选类型来防止在“一对一”关系中出现这种情况。
@ResourceRelationship var author: Person?
对于“一对多”关系,我们需要指示解码器忽略缺少的资源。
let decoder = JSONAPIDecoder()
decoder.ignoresMissingResources = true
let article = try decoder.decode(Article.self, from: json)
// Ignores any missing resources in the `comments` relationship
在解码多态关系时,如果解码器找到一个资源类型未包含在资源联合中,它会抛出一个 JSONAPIDecodingError.unhandledResourceType
错误。例如,考虑后端添加了一种客户端不知道的新型 Article
贡献者的情况。
"relationships": {
"contributors": {
"data": [
{
"type": "people",
"id": "12"
},
{
"type": "organizations",
"id": "25"
},
{
"type": "teams",
"id": "13"
},
]
}
}
我们必须指示解码器忽略未处理的资源类型以防止此错误。
let decoder = JSONAPIDecoder()
decoder.ignoresUnhandledResourceTypes = true
let article = try decoder.decode(Article.self, from: json)
// Ignores new types of contributors in the `contributors` relationship
此外,对于“一对一”关系,我们必须使用可选类型。
@ResourceRelationship var reviewer: Contributor?
JSONAPI
Swift 库已准备好投入生产,我们正在 Datadog iOS 应用程序中积极使用它。但是,在我们能够涵盖 JSON:API 规范的更多部分并进一步评估社区采用和反馈之前,我们将推迟发布版本 1.0
。
要在 Swift Package Manager 项目中使用 JSONAPI,请将以下行添加到 Package.swift
文件中的依赖项中
.package(url: "https://github.com/Datadog/swift-jsonapi", from: "0.1.0")
将 "JSONAPI"
作为可执行目标的依赖项包含在内
.target(name: "<target>", dependencies: [
.product(name: "JSONAPI", package: "swift-jsonapi")
]),
最后,将 import JSONAPI
添加到您的源代码中。
https://github.com/Datadog/swift-jsonapi