Exclude CodingKeys that doesn't need to be altered? - swift

Say I have a struct User model which has many properties in it.
struct User: Codable {
let firstName: String
let lastName: String
// many more properties...
}
As you can see above it conforms to Codable. Imagine if the lastName property is should be encoded/decoded as secondName and I would like to keep it as lastName at my end, I need to add the CodingKeys to the User model.
struct User: Codable {
//...
private enum CodingKeys: String, CodingKey {
case firstName
case lastName = "secondName"
// all the other cases...
}
}
Is there any possible way to avoid including all the cases in CodingKeys that have the same value as rawValue like the firstName in the above example (Feels redundant)? I know if I avoid the cases in CodingKeys it won't be included while decoding/encoding. But, is there a way I could override this behaviour?

There is a codable way, but the benefit is questionable.
Create a generic CodingKey
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }
init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue }
}
and add a custom keyDecodingStrategy
struct User: Codable {
let firstName: String
let lastName: String
let age : Int
}
let jsonString = """
{"firstName":"John", "secondName":"Doe", "age": 30}
"""
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ keyPath -> CodingKey in
let key = keyPath.last!
return key.stringValue == "secondName" ? AnyKey(stringValue:"lastName")! : key
})
let result = try decoder.decode(User.self, from: data)
print(result)
} catch {
print(error)
}

There is not such a feature at this time. But you can take advantage of using computed properties and make the original one private.
struct User: Codable {
var firstName: String
private var secondName: String
var lastName: String {
get { secondName }
set { secondName = newValue }
}
}
So no need to manual implementing of CodingKeys at all and it acts exactly like the way you like. Take a look at their counterparts:

Related

How to declare different structs inside one array in Swift

Is it possible to create an array of different structs?
My data structure looks like this:
enum MovementType: String, Codable {
case WeightMovement
case RepsMovement
}
struct Movement: Identifiable, Codable {
let id: UUID = UUID()
let name: String
let type: MovementType
let workouts: [WeightMovement, RepsMovement] ---> A solution for this line based on the Type above
}
struct WeightMovement: Identifiable, Codable {
let id: UUID = UUID()
let weight: Double
let sets: Int
let reps: Int
}
struct RepsMovement: Identifiable, Codable {
let id: UUID = UUID()
let sets: Int
let reps: Int
let seconds: Int
}
A brief example of what is should do:
A user can create multiple movements with a name and movementType. A user can add workouts to a movement but since each movementType holds different data i create different structs for that.
ideally the array will always hold only one type of Movement based on the type of the movement.
The easiest method would be to declare the properties whose presence is uncertain as optional and create a common struct that combines both of the optional properties. Since we already have a MovementType that would provide certainty about the availability of a particular property we could probably force-unwrap, although safe-unwrap is safer at any point.
struct Movement: Identifiable, Codable {
var id: UUID = UUID()
let name: String
let type: MovementType
let workouts: [MovementDetail]
}
struct MovementDetail: Identifiable, Codable {
var id: UUID = UUID()
let weight: Double?
let sets: Int
let reps: Int
let seconds: Int?
}
enum MovementType: String, Codable {
case WeightMovement
case RepsMovement
}
I would do it this way:
struct Movement: Identifiable, Codable {
var id: UUID = UUID()
var name: String
var type: MovementType
var weightWorkouts: [WeightMovement]
var repsWorkouts: [RepsMovement]
var workouts: [Codable] {
switch type {
case .WeightMovement:
return weightWorkouts
case .RepsMovement:
return repsWorkouts
}
}
}
This doesn't do exactly what you describe, as there are multiple array types on the struct, but for the object's consumer, you have access to a single property workouts that will return one of multiple possible types.
As Tushar points out in his comment, though, it is fragile to specify what the return type is in two different places (type and the actual type returned in the array). A subclass or protocol would probably be better.
ideally the array will always hold only one type of Movement based on the type of the movement.
In this case I recommend to decode the JSON manually and declare the type enum with associated types
enum MovementType {
case weight([WeightMovement])
case reps([RepsMovement])
}
struct Movement: Identifiable, Decodable {
private enum CodingKeys : String, CodingKey { case name, type, workouts }
let id: UUID = UUID()
let name: String
let type: MovementType
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let movementType = try container.decode(String.self, forKey: .type)
switch movementType {
case "WeightMovement":
let workouts = try container.decode([WeightMovement].self, forKey: .workouts)
type = .weight(workouts)
case "RepsMovement":
let workouts = try container.decode([RepsMovement].self, forKey: .workouts)
type = .reps(workouts)
default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid movement type")
}
}
}
struct WeightMovement: Identifiable, Decodable {
let id: UUID = UUID()
let weight: Double
let sets: Int
let reps: Int
}
struct RepsMovement: Identifiable, Decodable {
let id: UUID = UUID()
let sets: Int
let reps: Int
let seconds: Int
}
The explicit UUID assignment implies that id is not going to be decoded.

Codable Decodable JSON from String to Enum

I'm trying to decode a JSON using codable. I'm wondering if there's a way to customize codable to return HelloModel's typeCustomer as type TypeOfCustomerEnum instead of String?
Example:
{
"name": "Hello",
"lastName": "World",
"typeOfCustomer": "Student"
}
enum TypeOfCustomerEnum: String {
let Student = "Student"
let Paying = "Paying"
let NonPaying = "Nonpaying"
}
struct HelloModel: Codable {
let name: String
let lastName: String
let typeOfCustomer: TypeOfCustomerEnum // JSON for TypeOfCustomer is a String but TypeOfCustomer wanted
}
The type TypeOfCustomerEnum must also conform to Codable and the cases (must be cases) should be lowercased and the literal strings must match the JSON values
enum TypeOfCustomerEnum: String, Codable {
case student = "Student"
case paying = "Paying"
case nonPaying = "NonPaying"
}
struct HelloModel: Codable {
let name: String
let lastName: String
let typeOfCustomer: TypeOfCustomerEnum
}

Why not conform to protocol encodable decodable?

I have the following structure of code. If I omit the codingkey part the code is running. I implemented StringConverter to convert a string to Int (from bySundell Swift Side)
struct Video: Codable {
var title: String
var description: String
var url: URL
var thumbnailImageURL: URL
var numberOfLikes: Int {
get { return likes.value }
}
private var likes: StringBacked<Int>
enum CodingKeys: String, CodingKey{
case title = "xxx"
case description = "jjjj"
case url = "url"
case thumbnailImageURL = "jjjjjjjj"
case numberofLikes = "jjjjjkkkk"
}
}
// here the code for converting the likes
protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}
extension Int: StringRepresentable {}
struct StringBacked<Value: StringRepresentable>: Codable {
var value: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
let stringToConvert = string.split(separator: "/").last!.description
guard let value = Value(stringToConvert) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: """
Failed to convert an instance of \(Value.self) from "\(string)"
"""
)
}
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}}
As i said, if I omit the Codingkeys part it does me show no error. I would simply create a struct, where I get a string from a Rest API and I want to convert in an Int for my database using Codable.
Thanks
Arnold
When you define CodingKeys, you need to provide key for each non-optional/non-initialized property so that compiler know how to initialize while decoding. Applying this to Video, it will look like this,
struct Video: Codable {
var title: String
var description: String
var url: URL
var thumbnailImageURL: URL
var numberOfLikes: Int {
return likes.value
}
private var likes: StringBacked<Int>
enum CodingKeys: String, CodingKey{
case title = "xxx"
case description = "jjjj"
case url = "url"
case thumbnailImageURL = "jjjjjjjj"
case likes = "jjjjjkkkk"
}
}
If you see closely, this property private var likes: StringBacked<Int> was not provided any CodingKey in the enum so compiler was complaining. I updated the enum with this case case likes = "jjjjjkkkk" and removed case numberofLikes = "jjjjjkkkk" because numberofLikes is a read only computed property that doesn't need any parsing.

Decodable JSONSerialization error custom object alamofire

I have a custom APIClient using alamofire5 beta that conforms to Codable protocol for the request.
I'm trying to send a custom object via httpBody (post) and I'm getting this error:
Invalid type in JSON write (_SwiftValue)
This is the object that I'm trying to send:
struct Complex: Codable {
var id: String
var name: String
var address: String
var zipcode: String
var amenities: [String]
var schedules: [ComplexSchedules]
init(id: String, name: String, address: String, zipcode: String, amenities: [String], schedules: [ComplexSchedules]) {
self.id = id
self.name = name
self.address = address
self.zipcode = zipcode
self.amenities = amenities
self.schedules = schedules
}
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case address = "address"
case zipcode = "zipcode"
case amenities = "amenities"
case schedules = "schedules"
}
struct TimeRange: Codable {
var from: String
var to: String
init(from: String, to: String) {
self.from = from
self.to = to
}
enum CodingKeys: String, CodingKey {
case from = "from"
case to = "to"
}
}
struct ComplexSchedules: Codable {
var day: String
var timeRanges: [TimeRange]
init(day: String, timeRanges: [TimeRange]) {
self.day = day
self.timeRanges = timeRanges
}
enum CodingKeys: String, CodingKey {
case day = "day"
case timeRanges = "time_ranges"
}
}
}
It fails when I call this method:
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: complex, options: [])
Any thoughts?
You may need
do {
let data = try JSONEncoder().encode(complex)
urlRequest.httpBody = data
}
catch {
print(error)
}
as Codable is used to be able to utilize JSONDecoder and JSONEncoder not to use with JSONSerialization that expects a non-custom object like raw String/Array/Dictionary

Decodable conformance with property of enum type

I have this enum:
enum DealStatus:String {
case PENDING = "Pending"
case ACTIVE = "Active"
case STOP = "Stop"
case DECLINED = "Declined"
case PAUSED = "Paused"
}
and struct:
struct ActiveDeals: Decodable {
let keyword: String
let bookingType: String
let expiryDate: Int
let createdAt: Int?
let shopLocation: String?
let dealImages: [DealImages]?
let dealStatus: String?
let startingDate: Int?
}
In struct I am trying to assign enum as type for dealStatus like this:
struct ActiveDeals: Decodable {
let keyword: String
let bookingType: String
let expiryDate: Int
let createdAt: Int?
let shopLocation: String?
let dealImages: [DealImages]?
let dealStatus: DealStatus
let startingDate: Int?
}
But I am getting some compiler error:
Type 'ActiveDeals' does not conform to protocol 'Decodable'
Protocol requires initializer 'init(from:)' with type 'Decodable'
(Swift.Decodable)
Cannot automatically synthesize 'Decodable'
because 'DealStatus' does not conform to 'Decodable'
The problem is that Swift can automatically synthesize the methods needed for Decodable only if all the properties of a struct are also Decodable and your enum is not Decodable.
Having just had a go in a playground, it seems you can make your enum Decodable just by declaring that it is and Swift will automatically synthesize the methods for you. i.e.
enum DealStatus:String, Decodable
// ^^^^^^^^^ This is all you need
{
case PENDING = "Pending"
case ACTIVE = "Active"
case STOP = "Stop"
case DECLINED = "Declined"
case PAUSED = "Paused"
}
The error says mean that some of the properties of class do not conform to Decodable protocol.
Add Decodable conformance to your enum and it should be fine.
extension DealStatus: Decodable { }
According to Federico Zanetello on his post Swift 4 Decodable: Beyond The Basics, Codable and Decobable protocols will works fine if you need to parse a subset of primitives (strings, numbers, bools, etc).
On your case, just making DealStatus conform to Decodable (as suggested by
JeremyP) should solve your problem. You can check on Playgrounds creating your own JSON data and trying to parse it:
import UIKit
enum DealStatus: String, Decodable {
case PENDING = "Pending"
case ACTIVE = "Active"
case STOP = "Stop"
case DECLINED = "Declined"
case PAUSED = "Paused"
}
struct ActiveDeals: Decodable {
let keyword: String
let bookingType: String
let expiryDate: Int
let createdAt: Int?
let shopLocation: String?
let dealStatus: DealStatus
let startingDate: Int?
}
let json = """
{
"keyword": "Some keyword",
"bookingType": "A type",
"expiryDate": 123456,
"createdAt": null,
"shopLocation": null,
"dealStatus": "Declined",
"startingDate": 789456
}
""".data(using: .utf8)!
do {
let deal = try JSONDecoder().decode(ActiveDeals.self, from: json)
print(deal)
print(deal.dealStatus)
} catch {
print("error info: \(error)")
}
And the output will be:
ActiveDeals(keyword: "Some keyword", bookingType: "A type", expiryDate: 123456, createdAt: nil, shopLocation: nil, dealStatus: __lldb_expr_61.DealStatus.DECLINED, startingDate: Optional(789456))
DECLINED
However, to become a better programmer you should always be curious and try to learn how things, so if you are interested on how you could conform to Decodable protocol (let's say you need custom keys, custom errors or some more complex data structure), this is how you can do it:
import UIKit
enum DealStatus: String {
case PENDING = "Pending"
case ACTIVE = "Active"
case STOP = "Stop"
case DECLINED = "Declined"
case PAUSED = "Paused"
}
struct ActiveDeals {
let keyword: String
let bookingType: String
let expiryDate: Int
let createdAt: Int?
let shopLocation: String?
let dealStatus: DealStatus
let startingDate: Int?
}
extension ActiveDeals: Decodable {
enum StructKeys: String, CodingKey {
case keyword = "keyword"
case bookingType = "booking_type"
case expiryDate = "expiry_date"
case createdAt = "created_at"
case shopLocation = "shop_location"
case dealStatus = "deal_status"
case startingDate = "starting_date"
}
enum DecodingError: Error {
case dealStatus
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StructKeys.self)
let keyword = try container.decode(String.self, forKey: .keyword)
let bookingType = try container.decode(String.self, forKey: .bookingType)
let expiryDate = try container.decode(Int.self, forKey: .expiryDate)
let createdAt = try container.decode(Int?.self, forKey: .createdAt)
let shopLocation = try container.decode(String?.self, forKey: .shopLocation)
//Get deal status as a raw string and then convert to your custom enum
let dealStatusRaw = try container.decode(String.self, forKey: .dealStatus)
guard let dealStatus = DealStatus(rawValue: dealStatusRaw) else {
throw DecodingError.dealStatus
}
let startingDate = try container.decode(Int?.self, forKey: .startingDate)
self.init(keyword: keyword, bookingType: bookingType, expiryDate: expiryDate, createdAt: createdAt, shopLocation: shopLocation, dealStatus: dealStatus, startingDate: startingDate)
}
}
let json = """
{
"keyword": "Some keyword",
"booking_type": "A type",
"expiry_date": 123456,
"created_at": null,
"shop_location": null,
"deal_status": "Declined",
"starting_date": 789456
}
""".data(using: .utf8)!
do {
let deal = try JSONDecoder().decode(ActiveDeals.self, from: json)
print(deal)
print(deal.dealStatus)
} catch {
print("error info: \(error)")
}
In this case the output is still the same:
ActiveDeals(keyword: "Some keyword", bookingType: "A type", expiryDate: 123456, createdAt: nil, shopLocation: nil, dealStatus: __lldb_expr_67.DealStatus.DECLINED, startingDate: Optional(789456))
DECLINED