Currently, I have the following 2 JSON string - unoptimized_json
and optimized_json
.
let unoptimized_json = "[{\"id\":1,\"text\":\"hello\",\"checked\":true}]"
let optimized_json = "[{\"i\":1,\"t\":\"hello\",\"c\":true}]"
I would like to decode them, into same struct Object.
struct Checklist: Hashable, Codable {
let id: Int64
var text: String?
var checked: Bool
enum CodingKeys: String, CodingKey {
case id = "i"
case text = "t"
case checked = "c"
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Checklist, rhs: Checklist) -> Bool {
return lhs.id == rhs.id
}
}
However, current design can only accept format optimized_json
and not unoptimized_json
.
In Java Android, I can achieve this by using alternate
.
import com.google.gson.annotations.SerializedName;
public class Checklist {
@SerializedName(value="i", alternate="id")
private final long id;
@SerializedName(value="t", alternate="text")
private String text;
@SerializedName(value="c", alternate="checked")
private boolean checked;
}
I was wondering, in Swift, do we have equivalent feature to achieve so?
Is it possible to have JSON string with different field name decoded into same struct Object?
CodePudding user response:
There's no way to do that automatically, but you can implement your own decoding logic and attempt the different values there.
struct User: Decodable {
enum CodingKeys: String, CodingKey {
case i, id
}
let id: Int
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let i = try container.decodeIfPresent(Int.self, forKey: .i)
let id = try container.decodeIfPresent(Int.self, forKey: .id)
guard let identifier = i ?? id else { throw NoIDFoundError }
self.id = identifier
}
}
CodePudding user response:
I would suggest using a custom keyDecodingStrategy
when decoding and let that strategy return the correct key. This solution assumes that the names of the optimized keys are always the first letter(s) of the normal key. It can be used otherwise as well but then a more hard coded solution is needed.
First we use our own type for the keys, the interesting part is in init(stringValue: String)
where we use the CodingKeys
enum (see below) of CheckList
to get the right key
struct VaryingKey: CodingKey {
var intValue: Int?
init?(intValue: Int) {
self.intValue = intValue
stringValue = "\(intValue)"
}
var stringValue: String
init(stringValue: String) {
self.stringValue = Checklist.CodingKeys.allCases
.first(where: { stringValue == $0.stringValue.prefix(stringValue.count) })?
.stringValue ?? stringValue
}
}
We need to define a CodingKeys
enum for checklist and make it conform to CaseIterable
for the above code to work
enum CodingKeys: String, CodingKey, CaseIterable {
case id
case text
case type
case checked
}
and then we use this when decoding
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ codingPath in
let key = codingPath.last!.stringValue
return VaryingKey(stringValue: key)
})
do {
let unoptimized = try decoder.decode([Checklist].self, from: unoptimized_json.data(using: .utf8)!)
let optimized = try decoder.decode([Checklist].self, from: optimized_json.data(using: .utf8)!)
print(unoptimized)
print(optimized)
} catch {
print(error)
}
[__lldb_expr_377.Checklist(id: 1, text: Optional("hello"), type: "world", checked: true)]
[__lldb_expr_377.Checklist(id: 1, text: Optional("hello"), type: "world", checked: true)]
(The extra "type" attribute was only used to test the solution)