Home > Net >  Is it possible to have JSON string with different field name decoded into same struct Object?
Is it possible to have JSON string with different field name decoded into same struct Object?

Time:10-11

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)

  • Related