JSONDecoder 无法解码由 JSONEncoder 编码的对象

时间:2021-03-16 12:53:25

标签: json swift jsondecoder

我需要对SnakeCased JSON 进行编码/解码。我发现编码器正确编码 Value2 对象,但是解码器无法对其进行解码。我在这里做错了什么?

所需的 Json 格式:

{
  "address_line_1" : "Address",
  "full_name" : "Name",
  "id" : 2
}

代码:

struct Value1: Codable {
    let id: Int
    let fullName: String
    let addressLine1: String
}

struct Value2: Codable {
    let id: Int
    let fullName: String
    let addressLine_1: String
}

func printJson(_ object: Data) throws {
    let json = try JSONSerialization.jsonObject(with: object, options: [])
    let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
    print(String(data: data, encoding: .utf8)!)
}

func encode<T: Encodable>(_ object: T) throws -> Data {
    let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
    return try encoder.encode(object)
}

func decode<T: Decodable>(_ type: T.Type, from data: Data) throws {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    _ = try decoder.decode(type, from: data)
    print("✅ Decoded \(type) from:")
    try printJson(data)
}

do {
    var data: Data

    data = try encode(Value1(id: 1, fullName: "Name", addressLine1: "Address"))
    try decode(Value1.self, from: data)
    

    data = try encode(Value2(id: 2, fullName: "Name", addressLine_1: "Address"))
    _ = try decode(Value1.self, from: data)
    _ = try decode(Value2.self, from: data)
    
} catch {
    print("❌ Failed with error:", error)
}

输出:

✅ Decoded Value1 from:
{
  "address_line1" : "Address",
  "full_name" : "Name",
  "id" : 1
}
✅ Decoded Value1 from:
{
  "address_line_1" : "Address",
  "full_name" : "Name",
  "id" : 2
}
❌ Failed with error: keyNotFound(CodingKeys(stringValue: "addressLine_1", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"addressLine_1\", intValue: nil) (\"addressLine_1\"), with divergent representation addressLine1, converted to address_line_1.", underlyingError: nil))

2 个答案:

答案 0 :(得分:1)

convertFromSnakeCase 工作正常,您可以在第一次解码时检查它:

_ = try decode(Value1.self, from: data)

之后,当您尝试解码相同的 data 但使用 Value2 类型时,它肯定会失败,因为它期望不同的属性名称。这是您编码的蛇盒 JSON:

{
  "address_line_1" : "Address",
  "full_name" : "Name",
  "id" : 2
}

解码器转换后address_line_1变成addressLine1(同样适用于full_name),符合Value1的属性。如果您尝试解码 Value2 的相同数据,它会失败,因为属性名称需要 addressLine_1

在您的情况下,最佳策略是使用自定义编码键,如下所示:

struct Value2: Codable {
    private enum Value2CodingKey: String, CodingKey {
        case id
        case fullName = "full_name"
        case addressLine1 = "address_line_1"
    }

    let id: Int
    let fullName: String
    let addressLine1: String
}

答案 1 :(得分:0)

我找到了一个不使用自定义编码键的解决方案,而是使用自定义编码策略,因此编码人员也在数字之前处理 _。 这样 addressLine1 编码为 address_line_1address_line_1 解码为 addressLine1

用法:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCaseWithNumbers

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCaseWithNumbers

编码器实现:

extension JSONEncoder.KeyEncodingStrategy {
    static var convertToSnakeCaseWithNumbers: JSONEncoder.KeyEncodingStrategy {
        .custom { codingKeys -> CodingKey in
            let stringValue = codingKeys.last!.stringValue
            let newKey = AnyKey(stringValue: convertToSnakeCase(stringValue))!
            return newKey
        }
    }
    
    private static func convertToSnakeCase(_ stringKey: String) -> String {
        var key = stringKey
        let searchRange = key.index(after: key.startIndex)..<key.endIndex
        let nsRange = key.nsRange(from: searchRange)
        let matches = NSRegularExpression("([A-Z])|([0-9]+)").matches(in: key, options: [], range: nsRange)
        for match in matches.reversed() {
            guard let range = key.range(from: match.range) else { continue }
            key.insert("_", at: range.lowerBound)
        }
        return key.lowercased()
    }
}

extension JSONDecoder.KeyDecodingStrategy {
    static var convertFromSnakeCaseWithNumbers: JSONDecoder.KeyDecodingStrategy {
        .custom { (codingKeys) -> CodingKey in
            let stringValue = codingKeys.last!.stringValue
            let newKey = AnyKey(stringValue: convertFromSnakeCase(stringValue))!
            return newKey
        }
    }
    
    private static func convertFromSnakeCase(_ stringKey: String) -> String {
        guard stringKey.contains("_") else {
            return stringKey
        }
        let components = stringKey.split(separator: "_").map({ $0.firstCapitalized })
        return components.joined().firstLowercased
    }
}

private extension NSRegularExpression {
    convenience init(_ pattern: String) {
        do {
            try self.init(pattern: pattern)
        } catch {
            preconditionFailure("Illegal regular expression: \(pattern).")
        }
    }
}

private extension StringProtocol {
    var firstLowercased: String { prefix(1).lowercased() + dropFirst() }
    var firstCapitalized: String { prefix(1).capitalized + dropFirst() }
}

enum AnyKey: CodingKey {
    case string(String)
    case int(Int)
    
    var stringValue: String {
        switch self {
        case .string(let string):
            return string
        case .int(let int):
            return "\(int)"
        }
    }
    
    var intValue: Int? {
        guard case let .int(int) = self else { return nil }
        return int
    }
    
    init?(stringValue: String) {
        guard !stringValue.isEmpty else { return nil }
        self = .string(stringValue)
    }
    
    init?(intValue: Int) {
        self = .int(intValue)
    }
}

相关问题