Swift decodable with programatically provided coding keys - swift

This is a simplified model that is decoded from JSON:
struct Info: Decodable {
var text: String
var num: Int
}
struct Root: Decodable {
let info: Info
}
Sometimes I need to decode Info.text or Info.num only but sometimes both of them and to support all options I've made similar structs for decoding e.g:
// For text only
struct InfoText: Decodable {
var text: String
}
struct RootText: Decodable {
let info: InfoText
}
// For num only
struct InfoNum: Decodable {
var num: Int
}
struct RootNum: Decodable {
let info: InfoNum
}
This approach produces much cloned code and runtime checks to process the structs so is it possible to decode provided coding keys only with the single struct?

It's possible to provide any contextual information to the decoder with userInfo property and in this case we can pass an array of coding keys and use this info in the decoding process:
struct Info: Decodable {
var text: String?
var num: Int?
static var keys = CodingUserInfoKey(rawValue: "keys")!
enum CodingKeys: String, CodingKey {
case text, num
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let keys = decoder.userInfo[Self.keys] as? [CodingKeys] else {
return
}
if keys.contains(.text) {
text = try container.decode(String.self, forKey: .text)
}
if keys.contains(.num) {
num = try container.decode(Int.self, forKey: .num)
}
}
}
struct Root: Decodable {
let info: Info
}
let json = #"{ "info" : { "text": "Hello", "num": 20 } }"#.data(using: .utf8)!
let decoder = JSONDecoder()
let keys: [Info.CodingKeys] = [.text]
decoder.userInfo[Info.keys] = keys
let root = try decoder.decode(Root.self, from: json)
print(root)
// Outputs:
Root(info: Info(text: Optional("Hello"), num: nil))

Related

Decode JSON into a target struct which differs from the JSON model

I have the following JSON:
{
"header":{
"namespace":"Device",
"name":"Response",
"messageID":"60FA815A-DC432316",
"payloadVersion":"1"
},
"payload":{
"device":{
"firmware":"1.23W",
"name":"Device 1",
"uuid":"0ba64a0c-7a88b278-0001",
"security":{
"code":"aXdAPqd2OO9sZ6evLKjo2Q=="
}
},
"system":{
"uptime":5680126
}
}
}
I created the Swift structs using quicktype.io:
// MARK: - Welcome
struct Welcome: Codable {
let header: Header
let payload: Payload
}
// MARK: - Header
struct Header: Codable {
let namespace, name, messageID, payloadVersion: String
}
// MARK: - Payload
struct Payload: Codable {
let device: Device
let system: System
}
// MARK: - Device
struct Device: Codable {
let firmware, name, uuid: String
let security: Security
}
// MARK: - Security
struct Security: Codable {
let code: String
}
// MARK: - System
struct System: Codable {
let uptime: Int
}
However, I already have a Device type, that is a bit more minimal:
struct Device: Identifiable {
let id: UUID
let ip: String
let name: String
let firmware: String
let uptime: Double
// ...
}
How can I nicely decode the raw JSON data into my Device struct? Note that my Device is flat and has fields, that are more deeply nested in the original API response model. Do I a custom Decodable implementation?
You can create intermediate CodingKeys, but this often gets pretty tedious and unnecessary. Instead you can make a general-purpose "string-key" like:
struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral {
var stringValue: String
init(stringValue: String) { self.stringValue = stringValue }
init<S: StringProtocol>(_ stringValue: S) { self.init(stringValue: String(stringValue)) }
var intValue: Int?
init?(intValue: Int) { return nil }
init(stringLiteral value: String) { self.init(value) }
}
With that, you can navigate your structure pretty easily in a single decoder init by decoding nested containers:
extension Device: Decodable {
init(from decoder: Decoder) throws {
let root = try decoder.container(keyedBy: AnyStringKey.self)
let header = try root.nestedContainer(keyedBy: AnyStringKey.self, forKey: "header")
self.name = try header.decode(String.self, forKey: "name")
let payload = try root.nestedContainer(keyedBy: AnyStringKey.self, forKey: "payload")
let device = try payload.nestedContainer(keyedBy: AnyStringKey.self, forKey: "device")
self.id = try device.decode(UUID.self, forKey: "uuid")
self.firmware = try device.decode(String.self, forKey: "firmware")
let system = try payload.nestedContainer(keyedBy: AnyStringKey.self, forKey: "system")
self.uptime = try system.decode(Double.self, forKey: "uptime")
}
}
(I skipped ip because it's not in your data, and I assumed that your UUID was just a typo since it's not valid.)
With this, you should be able to decode any part you need.
This is very straightforward and standard, but if you have a lot of things to decode it can get a little tedious. You can improve it with a helper function in that case.
extension KeyedDecodingContainer {
func decode<T>(_ type: T.Type, forPath path: String) throws -> T
where T : Decodable, Key == AnyStringKey {
let components = path.split(separator: ".")
guard !components.isEmpty else {
throw DecodingError.keyNotFound(AnyStringKey(path),
.init(codingPath: codingPath,
debugDescription: "Could not find path \(path)",
underlyingError: nil))
}
if components.count == 1 {
return try decode(type, forKey: AnyStringKey(components[0]))
} else {
let container = try nestedContainer(keyedBy: AnyStringKey.self, forKey: AnyStringKey(components[0]))
return try container.decode(type, forPath: components.dropFirst().joined(separator: "."))
}
}
}
With this, you can access values by a dotted-path syntax:
extension Device: Decodable {
init(from decoder: Decoder) throws {
let root = try decoder.container(keyedBy: AnyStringKey.self)
self.name = try root.decode(String.self, forPath: "header.name")
self.id = try root.decode(UUID.self, forPath: "payload.device.uuid")
self.firmware = try root.decode(String.self, forPath: "payload.device.firmware")
self.uptime = try root.decode(Double.self, forPath: "payload.system.uptime")
}
}
I see two quick possible solutions:
Solution 1:
Rename the Codable Device:
struct Device: Codable {
...
}
into
struct DeviceFromAPI: Codable {
...
}
And then replace
struct Payload: Codable {
let device: Device
...
}
into
struct Payload: Codable {
let device: DeviceFromAPI
...
}
Solution2:
Use nested structures.
Put everything inside Welcome (which is the default QuickType.io name by the way, might be interesting to rename it).
struct Welcome: Codable {
let header: Header
let payload: Payload
// MARK: - Header
struct Header: Codable {
let namespace, name, messageID, payloadVersion: String
}
...
}
Go even if needed to put Device in Payload.
Then, you just have to use Welcome.Payload.Device or Welcome.Device (depending on how you nested it) when you want to refer to your Codable Device, and just Device when it's your own.
Then
Then, just have a custom init() for Device with the Codable Device as a parameter.
extension Device {
init(withCodableDevice codableDevice: DeviceFromAPI) {
self.firmware = codableDevice.firmware
...
}
}
or with solution 2:
extension Device {
init(withCodableDevice codableDevice: Welcome.Payload.Device) {
self.firmware = codableDevice.firmware
...
}
}

SwiftUI: Encode a struct to be saved in AppStorage

Currently trying to build my first app in swiftUI. The part I thought would be the easiest as become a nightmare... save a struct in AppStorage to be available upon restart of the app
I got two struct to save. The first is for player and I have implemented the RawRepresentable
struct Player: Codable, Identifiable {
let id: Int
let name: String
let gamePlayed: Int
let bestScore: Int
let nbrGameWon: Int
let nbrGameLost: Int
let totalScore: Int?
}
typealias PlayerList = [Player]
extension PlayerList: RawRepresentable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(PlayerList.self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
Calling in my view this way:
struct AddPlayerView: View {
#State var name: String = ""
#State var isDisabled: Bool = false
#State var modified: Bool = false
#AppStorage("players") var players: PlayerList = PlayerList()
...
}
The above works, now I also want to save the current game data, I have the following struct:
struct Game: Codable, Identifiable {
var id: Int
var currentPlayerIndexes: Int
var currentRoundIndex: Int?
var dealerIndex: Int?
var maxRounds: Int?
var dealResults: [Int: Array<PlayerRoundSelection>]?
var currentLeaderIds: Array<Int>?
var isGameInProgress: Bool?
}
extension Game: RawRepresentable {
public init?(rawValue: String) {
if rawValue == "" {
// did to fix issue when calling AppStorage, but it is probably a bad idea
self = Game(id:1, currentPlayerIndexes:1)
}
else {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Game.self, from: data)
else {
return nil
}
self = result
}
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return ""
}
return result
}
}
As soon as I try to modify the struct, it calls rawValue and the encoding fails with the following:
error: warning: couldn't get required object pointer (substituting NULL): Couldn't load 'self' because its value couldn't be evaluated
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=2, address=0x7ffee49bbff8).
Here part of the code that access the struct:
struct SelectPlayersView: View {
#AppStorage("currentGame") var currentGame: Game = Game(rawValue: "")!
....
NavigationLink(
destination: SelectModeTypeView(), tag: 2, selection: self.$selection) {
ActionButtonView(text:"Next", disabled: self.$isDisabled, buttonAction: {
var currentPlayers = Array<Int>()
self.players.forEach({ player in
if selectedPlayers.contains(player.id) {
currentPlayers.insert(player.id, at: currentPlayers.count)
}
})
// This used to be a list of indexes, but for testing only using a single index
self.currentGame.currentPlayerIndexes = 6
self.selection = 2
})
...
I found the code to encode here: https://lostmoa.com/blog/SaveCustomCodableTypesInAppStorageOrSceneStorage/
My understanding is that with the self in the encode, it generate an infinite loop hence the bad access.
I have really no knowledge how to properly encode this, any help, links would be appreciated
I had the same problem and I wanted to share my experience here.
I eventually found that apparently you cannot rely on the default Codable protocol implementation when used in combination with RawRepresentable.
So when I did my own Codable implementation, with CodingKeys and all, it worked!
I think your Codable implementation for Game would be something like:
enum CodingKeys: CodingKey {
case currentPlayerIndexes
case currentRoundIndex
// <all the other elements too>
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.currentPlayerIndexes = try container.decode(Int.self, forKey: .currentPlayerIndexes)
self.currentRoundIndex = try container.decode(Int.self, forKey: .currentRoundIndex)
// <and so on>
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(currentPlayerIndexes, forKey: .currentPlayerIndexes)
try container.encode(currentRoundIndex, forKey: .currentRoundIndex)
// <and so on>
}
I then wondered why your Player coding/decoding did work and found that the default coding and decoding of an Array (i.e. the PlayerList, which is [Player]), works fine.

How to decode JSON with objects as string values in Swift [duplicate]

How can I decode partially double serialized json string using Codable protocol?
class Person : Codable {
var name : String?
var hobby : String?
}
class Family : Codable {
var person: String?
var person_: Person?
}
class PerfectFamily : Codable {
var person: Person?
}
let jsonString = "{\"person\":\"{\\\"name\\\":\\\"Mike\\\",\\\"hobby\\\":\\\"fishing\\\"}\"}"
do {
// I could do this.
let family = try JSONDecoder().decode(Family.self, from: Data(jsonString.utf8))
family.person_ = try JSONDecoder().decode(Person.self, from: Data(family.person!.utf8))
print(family)
// However I want to write more simply like this. Do you have some idea?
let perfectFamily = try JSONDecoder().decode(PerfectFamily.self, from: Data(jsonString.utf8)) // error
print(perfectFamily)
} catch {
print(error)
}
If you can't fix your double encoded json you can provide your own custom decoder method to your PerfectFamily class but I recommend using a struct:
struct Person: Codable {
let name: String
let hobby: String
}
struct PerfectFamily: Codable {
let person: Person
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let person = try container.decode([String: String].self)["person"] ?? ""
self.person = try JSONDecoder().decode(Person.self, from: Data(person.utf8))
}
}
let json = "{\"person\":\"{\\\"name\\\":\\\"Mike\\\",\\\"hobby\\\":\\\"fishing\\\"}\"}"
do {
let person = try JSONDecoder().decode(PerfectFamily.self, from: Data(json.utf8)).person
print(person) // "Person(name: "Mike", hobby: "fishing")\n"
} catch {
print(error)
}

Decodable not working with non empty array

I'm using this library which has created non-empty collections like arrays, dictionaries and strings. https://github.com/pointfreeco/swift-nonempty
When I decode to the non-empty array I get the following error
Swift.DecodingError.typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "poiList", intValue: nil)], debugDescription: "Expected to decode Dictionary but found an array instead.", underlyingError: nil))
This is my structure
struct LocationCarModel: Codable {
// MARK: - Properties
var poiList: NonEmptyArray<PointOfInterest>
// MARK: - PointOfInterest
struct PointOfInterest: Codable {
var id: Int
var coordinate: Position
var fleetType: String
let numberPlate = "HCD837EC"
let model: String = "Tesla S"
let fuel: Double = 0.9
}
}
This is the response I'm getting https://fake-poi-api.mytaxi.com/?p2Lat=53.394655&p1Lon=9.757589&p1Lat=53.694865&p2Lon=10.099891
and this how I'm decoding it.
public extension Decodable {
static func parse(from item: Any?, strategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> Self? {
guard let data = self.data(from: item) else {
return nil
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = strategy
do {
let result = try decoder.decode(Self.self, from: data)
return result
} catch {
debugPrint(error)
return nil
}
}
private static func data(from item: Any?) -> Data? {
switch item {
case let data as Data:
return data
case let string as String:
return string.data(using: .utf8)
case .some(let item):
return try? JSONSerialization.data(withJSONObject: item, options: [])
case nil:
return nil
}
}
}
and the line to decode using the above function is
let model = LocationCarModel.parse(from: data)
Changing poiList to the standard swift array then no error occurs.
Any ideas how I can solve this issue? Any help would be appreciated. Thank you in advance.
You need to have your own init(from:) for the top struct since JSONDecoder doesn't understand NonEmpty and how to initialise it. Apart from the init I also added coding keys and a new error
enum DecodeError: Error {
case arrayIsEmptyError
}
struct LocationCarModel: Codable {
var poiList: NonEmpty<[PointOfInterest]>
enum CodingKeys: String, CodingKey {
case poiList
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let array = try container.decode([PointOfInterest].self, forKey: .poiList)
guard let first = array.first else {
throw DecodeError.arrayIsEmptyError
}
poiList = NonEmptyArray(first, array)
}
//...
}
You can try
struct Root: Codable {
let poiList: [PoiList]
}
// MARK: - PoiList
struct PoiList: Codable {
let id: Int
let coordinate: Coordinate
let fleetType: String
let heading: Double
}
// MARK: - Coordinate
struct Coordinate: Codable {
let latitude, longitude: Double
}
let res = try? JSONDecoder().decode(Root.self,from:data)
print(res)

Extending a class model for generic type Swift

I am passing API response with Moya and getting this value. I am able to get the object but I extented a base response to handle extra parameters but the extended value does not seem to work. The data expected could be an array of objects and it could just be a regular object. After passing this values, It stopped working and data is not got but every other parameter like status , message are passed except data. Here is my Base response and how I used it
class MaxResponseBase: Codable {
var status: String?
var message: String?
var pagination: Pagination?
var isSucessful: Bool {
return status == "success"
}
struct ErrorMessage {
static let passwordInvalid = " Current password is invalid."
static let loginErrorIncorrectInfo = " Incorrect username/password."
static let loginErrorAccountNotExist = " Invalid request"
}
}
class MaxResponse<T: Codable>: MaxResponseBase {
var data: T?
}
class MaxArrayResponse<T: Codable>: MaxResponseBase {
var data = [T]()
}
Here is my API call for signin for example
func signin(email: String, password: String) -> Observable<MaxResponse<AuthResponse>> {
return provider.rx.request(.signin(username: email, password: password))
.filterSuccess()
.mapObject(MaxResponse<AuthResponse>.self)
.asObservable()
}
how can I tweak this to get data object also
JSON
{
"status" : "success",
"data" : {
"is_locked" : false,
"__v" : 0,
"created_at" : "2019-04-15T11:57:12.551Z"
}
}
It could also be an array of data
(Note: all the code below can be put in a Playground to show that it works.)
In order to solve this, you have to manually write all your initializers. I posted the code that does most of it below but I strongly recommend you use structs instead of classes. It is better in every way if you use structs and containment instead of classes and inheritance.
struct Pagination: Codable { }
struct AuthResponse: Codable {
let isLocked: Bool
let __v: Int
let createdAt: Date
}
class MaxResponseBase: Codable {
let status: String?
let message: String?
let pagination: Pagination?
var isSucessful: Bool {
return status == "success"
}
struct ErrorMessage {
static let passwordInvalid = " Current password is invalid."
static let loginErrorIncorrectInfo = " Incorrect username/password."
static let loginErrorAccountNotExist = " Invalid request"
}
enum CodingKeys: String, CodingKey {
case status, message, pagination
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(String?.self, forKey: .status)
message = try? container.decode(String?.self, forKey: .message) ?? nil
pagination = try? container.decode(Pagination?.self, forKey: .pagination) ?? nil
}
}
class MaxResponse<T: Codable>: MaxResponseBase {
let data: T?
enum DataCodingKeys: String, CodingKey {
case data
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DataCodingKeys.self)
data = try container.decode(T?.self, forKey: .data)
try super.init(from: decoder)
}
}
let json = """
{
"status" : "success",
"data" : {
"is_locked" : false,
"__v" : 0,
"created_at" : "2019-04-15T11:57:12.551Z"
}
}
"""
let data = json.data(using: .utf8)!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ss.SSZ"
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let response = try decoder.decode(MaxResponse<AuthResponse>.self, from: data)
print(response)
It is far simpler and less code to just use a struct:
struct AuthResponse: Codable {
struct ResponseData: Codable {
let isLocked: Bool
let __v: Int
let createdAt: Date
}
let status: String?
let data: ResponseData
}
let json = """
{
"status" : "success",
"data" : {
"is_locked" : false,
"__v" : 0,
"created_at" : "2019-04-15T11:57:12.551Z"
}
}
"""
let data = json.data(using: .utf8)!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ss.SSZ"
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let response = try decoder.decode(AuthResponse.self, from: data)
print(response)
And if you really need the MaxResponse type, then make it a protocol and have your other types conform to it. I'm almost willing to bet that you don't need it though.
In response to your comments, here is a generic solution using structs:
struct LoginResponseData: Codable {
let isLocked: Bool
let __v: Int
let createdAt: Date
}
struct BlogResponseData: Codable {
let xxx: Bool
let yyy: Int
let createdAt: Date
}
struct BaseRresponse<ResponseData: Codable>: Codable {
let status: String?
let data: ResponseData
}