Vapor4 Content (Codable) 总是解码为空结果
Vapor4 Content (Codable) always decodes into empty result
我在我的 Vapor 4 应用程序中检测到奇怪的解码行为。
我们有一个端点接受一个简单的 JSON 作为查询参数:
/api/debug/filter?where=%7B%20%22or%22:%20%5B%7B%20%22amount%22:%20%7B%20%22gte%22:%20%220%22%20%7D%20%7D,%20%7B%20%22amount%22:%20%7B%20%22lte%22:%20100%20%7D%20%7D%5D%7D&sort=amount%20ASC&limit=10&offset=0
此处的 'where' 参数为:
{ "or": [{ "amount": { "gte": "0" } }, { "amount": { "lte": 100 } }]}
这是控制器Class:
import Vapor
struct DebugController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.group("debug") { (debug) in
debug.get("filter", use: debugFilter)
}
}
private func debugFilter(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
log.debug(req.query)
let whereString = try req.query.get(String.self, at: "where")
let whereValue = try req.query.get(Where.self, at: "where")
log.debug("Input String: \(whereString)")
log.debug("Input Value: \(whereValue)")
let filter = try req.query.decode(Filter.self)
let `where` = try req.query.decode(Where.self)
log.debug("Filter: \(filter)")
log.debug("Where: \(`where`)")
return req.eventLoop.makeSucceededFuture(()).transform(to: .ok)
}
}
输出:
21:23:31.844 DEBUG DebugController.debugFilter():19 - _URLQueryContainer(request: GET /api/debug/filter?where=%7B%20%22or%22:%20%5B%7B%20%22amount%22:%20%7B%20%22gte%22:%20%220%22%20%7D%20%7D,%20%7B%20%22amount%22:%20%7B%20%22lte%22:%20100%20%7D%20%7D%5D%7D&sort=amount%20ASC&limit=10&offset=0 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.193 Safari/537.36
Origin: http://localhost:4200
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:4200/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,de;q=0.7
)
21:23:31.857 DEBUG DebugController.debugFilter():22 - Input String: { "or": [{ "amount": { "gte": "0" } }, { "amount": { "lte": 100 } }]}
21:23:31.857 DEBUG DebugController.debugFilter():23 - Input Value: and([])
21:23:31.857 DEBUG DebugController.debugFilter():27 - Filter: Filter(where: Optional(App.Where.and([])), sort: Optional([App.Sort(key: "amount", direction: App.Sort.Direction.ascending)]), limit: Optional(10), offset: Optional(0))
21:23:31.858 DEBUG DebugController.debugFilter():28 - Where: and([])
此payload的Codable如下,过去解决得很好:
public indirect enum Where: Codable {
case expression(String, ExpressionRelation, Any?)
case and([Where])
case or([Where])
public enum ConjunctionKeys: String, CodingKey {
case or
case and
}
public enum ExpressionRelation: String {
case gt
case gte
case lt
case lte
case like
case eq
case isNull
}
public init(from decoder: Decoder) throws {
let conjunctionContainer = try? decoder.container(keyedBy: ConjunctionKeys.self)
// result is: [:] no matter what input was given, strange.
if let and = try? conjunctionContainer?.decode([Where].self, forKey: .and) {
self = .and(and)
return
}
// result is: [:] no matter what input was given, strange.
if let or = try? conjunctionContainer?.decode([Where].self, forKey: .or) {
self = .or(or)
return
}
// result is: [:] no matter what input was given, strange.
if let expressionContainer = try? decoder.singleValueContainer() {
if let expression = try? expressionContainer.decode([String: [String: String]].self),
let key = expression.keys.first,
let relationContainer = expression[key],
let relationKey = relationContainer.keys.first,
let relation = ExpressionRelation(rawValue: relationKey),
let value = relationContainer[relation.rawValue]
{
self = .expression(key, relation, value)
return
}
if let expression = try? expressionContainer.decode([String: [String: Double]].self),
let key = expression.keys.first,
let relationContainer = expression[key],
let relationKey = relationContainer.keys.first,
let relation = ExpressionRelation(rawValue: relationKey),
let value = relationContainer[relation.rawValue]
{
self = .expression(key, relation, value)
return
}
if let expression = try? expressionContainer.decode([String: [String: Int]].self),
let key = expression.keys.first,
let relationContainer = expression[key],
let relationKey = relationContainer.keys.first,
let relation = ExpressionRelation(rawValue: relationKey),
let value = relationContainer[relation.rawValue]
{
self = .expression(key, relation, value)
return
}
}
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Unable to decode", underlyingError: nil))
}
public func encode(to encoder: Encoder) throws {
switch self {
case .and(let and):
var conjunctionContainer = encoder.container(keyedBy: ConjunctionKeys.self)
try conjunctionContainer.encode(and, forKey: .and)
case .or(let or):
var conjunctionContainer = encoder.container(keyedBy: ConjunctionKeys.self)
try conjunctionContainer.encode(or, forKey: .or)
case .expression(let key, let relation, let value):
var singleValueContainer = encoder.singleValueContainer()
switch value {
case is String:
try singleValueContainer.encode([key: [relation.rawValue: value as! String]])
case is Double:
try singleValueContainer.encode([key: [relation.rawValue: value as! Double]])
case is Int:
try singleValueContainer.encode([key: [relation.rawValue: value as! Int]])
default: break
}
}
}
}
这里的问题是每个 .decode()
调用 returns [:]
而不是 nil
因此无论输入什么,解码的内容总是 and([])
给出了。
奇怪的是它在项目中的 Playground 中运行得很好:
- 项目中没有配置自定义JSON解码器。
- 我正在使用最新的 xCode:版本 11.7 (11E801a)
这里是用到的包:
// swift-tools-version:5.2
import PackageDescription
let package = Package(
name: "backend",
platforms: [
.macOS(.v10_15)
],
dependencies: [
// A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.35.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
.package(url: "https://github.com/vapor/jwt.git", from: "4.0.0-rc.2"),
.package(url: "https://github.com/iLem0n/SwiftyBeaver.git", .exact("1.8.4")),
.package(url: "../SwiftSpec", .branch("master")),
.package(url: "https://github.com/Maxim-Inv/SwiftDate.git", .branch("master")),
.package(url: "https://github.com/vapor/queues.git", from: "1.0.0"),
.package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0-rc.1"),
.package(url: "https://github.com/dehesa/CodableCSV", from: "0.6.2")
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
.product(name: "Vapor", package: "vapor"),
.product(name: "JWT", package: "jwt"),
.product(name: "SwiftyBeaver", package: "SwiftyBeaver"),
.product(name: "SwiftDate", package: "SwiftDate"),
.product(name: "SwiftSpec", package: "SwiftSpec"),
.product(name: "CodableCSV", package: "CodableCSV"),
.product(name: "Queues", package: "queues"),
.product(name: "QueuesRedisDriver", package: "queues-redis-driver")
],
swiftSettings: [
// Enable better optimizations when building in Release configuration. Despite the use of
// the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
// builds. See <https://github.com/swift-server/guides#building-for-production> for details.
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
]
),
.target(name: "Run", dependencies: [.target(name: "App")]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)
有谁知道出了什么问题或者我可以在哪里继续调试?
表单解码与JSON解码不同。所以你混合了两种不同的格式,这就是问题的根源。我建议不要尝试将 JSON 放入查询中,因为您总是会遇到边缘情况和解码问题。
但是,如果您必须这样做,您就快完成了。您需要做的是获取原始字符串(它将 URL 编码的字符串转换为 JSON 字符串),然后使用 JSONDecoder
手动对其进行解码 - 这应该会为您提供所需的结果。
我在我的 Vapor 4 应用程序中检测到奇怪的解码行为。
我们有一个端点接受一个简单的 JSON 作为查询参数:
/api/debug/filter?where=%7B%20%22or%22:%20%5B%7B%20%22amount%22:%20%7B%20%22gte%22:%20%220%22%20%7D%20%7D,%20%7B%20%22amount%22:%20%7B%20%22lte%22:%20100%20%7D%20%7D%5D%7D&sort=amount%20ASC&limit=10&offset=0
此处的 'where' 参数为:
{ "or": [{ "amount": { "gte": "0" } }, { "amount": { "lte": 100 } }]}
这是控制器Class:
import Vapor
struct DebugController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.group("debug") { (debug) in
debug.get("filter", use: debugFilter)
}
}
private func debugFilter(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
log.debug(req.query)
let whereString = try req.query.get(String.self, at: "where")
let whereValue = try req.query.get(Where.self, at: "where")
log.debug("Input String: \(whereString)")
log.debug("Input Value: \(whereValue)")
let filter = try req.query.decode(Filter.self)
let `where` = try req.query.decode(Where.self)
log.debug("Filter: \(filter)")
log.debug("Where: \(`where`)")
return req.eventLoop.makeSucceededFuture(()).transform(to: .ok)
}
}
输出:
21:23:31.844 DEBUG DebugController.debugFilter():19 - _URLQueryContainer(request: GET /api/debug/filter?where=%7B%20%22or%22:%20%5B%7B%20%22amount%22:%20%7B%20%22gte%22:%20%220%22%20%7D%20%7D,%20%7B%20%22amount%22:%20%7B%20%22lte%22:%20100%20%7D%20%7D%5D%7D&sort=amount%20ASC&limit=10&offset=0 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.193 Safari/537.36
Origin: http://localhost:4200
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:4200/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,de;q=0.7
)
21:23:31.857 DEBUG DebugController.debugFilter():22 - Input String: { "or": [{ "amount": { "gte": "0" } }, { "amount": { "lte": 100 } }]}
21:23:31.857 DEBUG DebugController.debugFilter():23 - Input Value: and([])
21:23:31.857 DEBUG DebugController.debugFilter():27 - Filter: Filter(where: Optional(App.Where.and([])), sort: Optional([App.Sort(key: "amount", direction: App.Sort.Direction.ascending)]), limit: Optional(10), offset: Optional(0))
21:23:31.858 DEBUG DebugController.debugFilter():28 - Where: and([])
此payload的Codable如下,过去解决得很好:
public indirect enum Where: Codable {
case expression(String, ExpressionRelation, Any?)
case and([Where])
case or([Where])
public enum ConjunctionKeys: String, CodingKey {
case or
case and
}
public enum ExpressionRelation: String {
case gt
case gte
case lt
case lte
case like
case eq
case isNull
}
public init(from decoder: Decoder) throws {
let conjunctionContainer = try? decoder.container(keyedBy: ConjunctionKeys.self)
// result is: [:] no matter what input was given, strange.
if let and = try? conjunctionContainer?.decode([Where].self, forKey: .and) {
self = .and(and)
return
}
// result is: [:] no matter what input was given, strange.
if let or = try? conjunctionContainer?.decode([Where].self, forKey: .or) {
self = .or(or)
return
}
// result is: [:] no matter what input was given, strange.
if let expressionContainer = try? decoder.singleValueContainer() {
if let expression = try? expressionContainer.decode([String: [String: String]].self),
let key = expression.keys.first,
let relationContainer = expression[key],
let relationKey = relationContainer.keys.first,
let relation = ExpressionRelation(rawValue: relationKey),
let value = relationContainer[relation.rawValue]
{
self = .expression(key, relation, value)
return
}
if let expression = try? expressionContainer.decode([String: [String: Double]].self),
let key = expression.keys.first,
let relationContainer = expression[key],
let relationKey = relationContainer.keys.first,
let relation = ExpressionRelation(rawValue: relationKey),
let value = relationContainer[relation.rawValue]
{
self = .expression(key, relation, value)
return
}
if let expression = try? expressionContainer.decode([String: [String: Int]].self),
let key = expression.keys.first,
let relationContainer = expression[key],
let relationKey = relationContainer.keys.first,
let relation = ExpressionRelation(rawValue: relationKey),
let value = relationContainer[relation.rawValue]
{
self = .expression(key, relation, value)
return
}
}
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Unable to decode", underlyingError: nil))
}
public func encode(to encoder: Encoder) throws {
switch self {
case .and(let and):
var conjunctionContainer = encoder.container(keyedBy: ConjunctionKeys.self)
try conjunctionContainer.encode(and, forKey: .and)
case .or(let or):
var conjunctionContainer = encoder.container(keyedBy: ConjunctionKeys.self)
try conjunctionContainer.encode(or, forKey: .or)
case .expression(let key, let relation, let value):
var singleValueContainer = encoder.singleValueContainer()
switch value {
case is String:
try singleValueContainer.encode([key: [relation.rawValue: value as! String]])
case is Double:
try singleValueContainer.encode([key: [relation.rawValue: value as! Double]])
case is Int:
try singleValueContainer.encode([key: [relation.rawValue: value as! Int]])
default: break
}
}
}
}
这里的问题是每个 .decode()
调用 returns [:]
而不是 nil
因此无论输入什么,解码的内容总是 and([])
给出了。
奇怪的是它在项目中的 Playground 中运行得很好:
- 项目中没有配置自定义JSON解码器。
- 我正在使用最新的 xCode:版本 11.7 (11E801a)
这里是用到的包:
// swift-tools-version:5.2
import PackageDescription
let package = Package(
name: "backend",
platforms: [
.macOS(.v10_15)
],
dependencies: [
// A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.35.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
.package(url: "https://github.com/vapor/jwt.git", from: "4.0.0-rc.2"),
.package(url: "https://github.com/iLem0n/SwiftyBeaver.git", .exact("1.8.4")),
.package(url: "../SwiftSpec", .branch("master")),
.package(url: "https://github.com/Maxim-Inv/SwiftDate.git", .branch("master")),
.package(url: "https://github.com/vapor/queues.git", from: "1.0.0"),
.package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0-rc.1"),
.package(url: "https://github.com/dehesa/CodableCSV", from: "0.6.2")
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
.product(name: "Vapor", package: "vapor"),
.product(name: "JWT", package: "jwt"),
.product(name: "SwiftyBeaver", package: "SwiftyBeaver"),
.product(name: "SwiftDate", package: "SwiftDate"),
.product(name: "SwiftSpec", package: "SwiftSpec"),
.product(name: "CodableCSV", package: "CodableCSV"),
.product(name: "Queues", package: "queues"),
.product(name: "QueuesRedisDriver", package: "queues-redis-driver")
],
swiftSettings: [
// Enable better optimizations when building in Release configuration. Despite the use of
// the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
// builds. See <https://github.com/swift-server/guides#building-for-production> for details.
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
]
),
.target(name: "Run", dependencies: [.target(name: "App")]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)
有谁知道出了什么问题或者我可以在哪里继续调试?
表单解码与JSON解码不同。所以你混合了两种不同的格式,这就是问题的根源。我建议不要尝试将 JSON 放入查询中,因为您总是会遇到边缘情况和解码问题。
但是,如果您必须这样做,您就快完成了。您需要做的是获取原始字符串(它将 URL 编码的字符串转换为 JSON 字符串),然后使用 JSONDecoder
手动对其进行解码 - 这应该会为您提供所需的结果。