我需要对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))
答案 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_1
,address_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)
}
}