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 中运行得很好:

这里是用到的包:

// 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 手动对其进行解码 - 这应该会为您提供所需的结果。