Can I use both init() and initial value in Decoder? - ios

I have lots of values in my codable struct. I have URLs coming in as ""(empty string) so I need custom Decoder to convert "" as nil. So I made a propertyWrapper to solve this.
For example, I have values like ["https://google.com", "", "https://google.com"] and I want to make it as [URL?]. This works well with my decoder. It's converted as [URL("https://google.com"), nil, URL("https://google.com")]
However, I found a problem that, when I use init(from decoder: Decoder) throws, I also have to initialize all other values in struct. Is there any way to use just courseImages and use other values in struct as set?
struct CabinetCourse: Codable {
let courseId: String
let title: String
let planStartDate: Date
let planEndDate: Date
let companionTypeCd: String
let courseCategory: String
let planId: String
let nickname: String?
let isCabinet: Bool
let isFavorite: Bool?
let course: String
let score: String
let shareCnt: Int
let favoriteCnt: Int
let cabinetCnt: Int
let placeCount: Int
let createDt: Date
let childPlaceCount: Int
let wheelChairPlaceCount: Int
let elderPlaceCount: Int
#OptionalObject
var courseImages: [URL?]
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let emptyURLS = try values.decode([OptionalObject<URL>].self, forKey: .courseImages)
courseImages = emptyURLS.map { $0.wrappedValue }
// => these are the lines I don't want to write
courseId = try values.decode(String.self, forKey: .courseId)
title = try values.decode(String.self, forKey: .title)
planStartDate = try values.decode(Date.self, forKey: .planStartDate)
planEndDate = try values.decode(Date.self, forKey: .planEndDate)
companionTypeCd = try values.decode(String.self, forKey: .companionTypeCd)
courseCategory = try values.decode(String.self, forKey: .courseCategory)
planId = try values.decode(String.self, forKey: .planId)
nickname = try values.decode(String.self, forKey: .nickname)
isCabinet = try values.decode(Bool.self, forKey: .isCabinet)
isFavorite = try values.decode(Bool.self, forKey: .isCabinet)
course = try values.decode(String.self, forKey: .isCabinet)
score = try values.decode(String.self, forKey: .isCabinet)
}
#propertyWrapper
struct OptionalObject<Base: Decodable>: Decodable {
var wrappedValue: Base?
init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
wrappedValue = try container.decode(Base.self)
} catch {
wrappedValue = nil
}
}
}

You are misunderstanding property wrappers. They "decorate" the entire property type - [URL?], not just the array element type URL?. [URL?] doesn't match the type of wrappedValue. [URL]? does (Base == [URL]), but that's not what you want.
One way to create a property wrapper that can be applied to an array of optionals is:
#propertyWrapper
struct OptionalArray<Base: Decodable>: Decodable {
var wrappedValue: [Base?]
...
Now Base == URL matches [URL?], and in init, you have to decode a [Base?]:
init(from decoder: Decoder) throws {
var arr = [Base?]()
var container = try decoder.unkeyedContainer()
for _ in 0..<(container.count ?? 0) {
if let element = try? container.decode(Base.self) {
arr.append(element)
} else {
arr.append(nil)
_ = try container.decode(String.self) // advances the decoder to the next position
}
}
wrappedValue = arr
}
Once you have the property wrapper, you don't need the custom decoding code at all. Swift figures it out.
struct CabinetCourse: Decodable {
let courseId: String
let title: String
let planStartDate: Date
let planEndDate: Date
let companionTypeCd: String
let courseCategory: String
let planId: String
let nickname: String?
let isCabinet: Bool
let isFavorite: Bool?
let course: String
let score: String
let shareCnt: Int
let favoriteCnt: Int
let cabinetCnt: Int
let placeCount: Int
let createDt: Date
let childPlaceCount: Int
let wheelChairPlaceCount: Int
let elderPlaceCount: Int
#OptionalArray
var courseImages: [URL?]
}
// That's it!
For the encoding part, it depends on how you want to encode the nils. But either way, the code is very similar to the decoding code.

Related

working with similar json objects, casting int to string [duplicate]

This question already has answers here:
Using codable with value that is sometimes an Int and other times a String
(5 answers)
Closed last year.
i am working with a service that have both a websocket for live data and a api for historical data
the JSON looks similar and i would like to decode it to the same object
the only difference is that in the live one variable is a number but as a string and with the historical data the number is an int.
and preferably i would like to not have to create 2 almost identical decodable objects.
have anyone tried something similar.
You have to define a single type (Int or String) for your data structure and use init with Decoder to make a custom parsing.
struct MyData: Decodable {
let value: Int // Could be Int or String from different services
}
extension MyData {
enum CodingKeys: String, CodingKey {
case value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
value = try container.decode(Int.self, forKey: .value)
} catch {
let stringValue = try container.decode(String.self, forKey: .value)
if let valueInt = Int(stringValue) {
value = valueInt
} else {
var codingPath = container.codingPath
codingPath.append(CodingKeys.value)
let debugDescription = "Could not create Int from String \(stringValue) of field \(CodingKeys.value.rawValue)"
let context = DecodingError.Context(codingPath: codingPath, debugDescription: debugDescription)
throw DecodingError.dataCorrupted(context)
}
}
}
}
I think you need a wrapper for that case of some sort. To make it as convenient as possible you could use a property wrapper for this
#propertyWrapper
struct NormalOrStringyInt: Decodable {
var wrappedValue: Int?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Int.self) {
wrappedValue = value
} else if
let string = try? container.decode(String.self),
let value = Int(string)
{
wrappedValue = value
} else {
wrappedValue = nil // default value
}
}
}
struct Model: Codable {
#NormalOrStringyInt var id: Int?
var someInt: Int
var someString: String
...
}
let model = try! JSONDecoder().decode(Model, from: data)
let id: Int? = model.id.wrappedValue

How to decode DynamicKeys & CodingKeys in the same container?

Consider the following JSON: I'm trying to decode the "teams" object.
let jsonString = """
{
"Superheroes":{
"Marvel":"107",
"DC":"106"
},
"teams":{
"106":{
"name":"Marvel",
"Superheroes":{
"890":{
"name":"Batman"
}
}
},
"107":{
"name":"DC",
"Superheroes":{
"891":{
"name":"Wonder Woman"
}
}
}
}
}
"""
I have tried something like this:
struct SuperheroResponse: Decodable {
let teams: [Team]
private enum CodingKeys: String, CodingKey {
case teams = "teams"
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let teamContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.teams)
print(teamContainer.allKeys.count)
let tempArray: [Team] = []
for key in teamContainer.allKeys {
let decodedObject = try teamContainer.decode(Team.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
teams = tempArray
}
}
struct Team: Decodable {
let name: String
}
I thought that first I would get the teams container, map over the keys and go on from there. Problem is teamContainer.allKeys.count is always zero.
Also the following line, results in following error: Cannot convert value of type 'SuperheroResponse.DynamicCodingKeys' to expected argument type 'SuperheroResponse.CodingKeys'
let decodedObject = try teamContainer.decode(Team.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
Finally I decode it as follows:
let jsonData = Data(jsonString.utf8)
let decodedResult = try! JSONDecoder().decode(SuperheroResponse.self, from: jsonData)
dump(decodedResult)
Any help would be appreciated. Ideally I would like something like SuperheroResponse -> [Team],
Team -> name, [Superhero], Superhero -> name
You just have a couple of minor mistakes. You're almost there.
The team container is keyed by DynamicCodingKeys:
let teamContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, // <=
forKey: .teams)
And the Teams can be decoded as using the key you're given:
let decodedObject = try teamContainer.decode(Team.self, forKey: key)
Also, tempArray needs to be var:
var tempArray: [Team] = []
Or replace that loop with a map:
teams = try teamContainer.allKeys.map {
try teamContainer.decode(Team.self, forKey: $0)
}
All together:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let teamContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .teams)
teams = try teamContainer.allKeys.map {
try teamContainer.decode(Team.self, forKey: $0)
}
}

Handle string and int received in json [duplicate]

This question already has answers here:
Swift 4 Codable - Bool or String values
(1 answer)
Swift 4 Codable - API provides sometimes an Int sometimes a String
(1 answer)
Closed 2 years ago.
I have a json which looks like so...
{
id = 123456;
isDeleted = 0;
name = testName;
parentId = "<null>"; // This can also be integer
plantId = 1223; // This can also be string
type = 1;
}
In the response above, I can get either a string or an int for both parentId & plantId. How can I handle both the cases..?
This is how my structure looks...
struct Root : Decodable {
let organizations : [Organization1]
}
struct Organization1 : Decodable {
let id: Int
let isDeleted: Bool
let name: String
let parentId: Int?
let type: Int
let plantId: String?
let loggedInUserId: Int?
}
You can do that like this
struct GeneralProduct: Decodable {
let id: Int
let isDeleted: Bool
let name: String
let parentId: String?
let type: Int
let plantId: String?
let loggedInUserId: Int?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
isDeleted = try container.decode(Bool.self, forKey: .isDeleted)
name = try container.decode(String.self, forKey: .id)
type = try container.decode(Int.self, forKey: .type)
loggedInUserId = try container.decode(Int.self, forKey: .loggedInUserId)
if let value = try? container.decode(Int.self, forKey: .parentId) {
parentId = String(value)
} else {
parentId = try container.decode(String.self, forKey: .id)
}
if let value = try? container.decode(Int.self, forKey: .plantId) {
plantId = String(value)
} else {
plantId = try container.decode(String.self, forKey: .id)
}
}
}

How to Decode selected keys manually and rest with the automatic decoding with swift Decodable?

Here is the code I am using,
struct CreatePostResponseModel : Codable{
var transcodeId:String?
var id:String = ""
enum TopLevelCodingKeys: String, CodingKey {
case _transcode = "_transcode"
case _transcoder = "_transcoder"
}
enum CodingKeys:String, CodingKey{
case id = "_id"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: TopLevelCodingKeys.self)
if let transcodeId = try container.decodeIfPresent(String.self, forKey: ._transcode) {
self.transcodeId = transcodeId
}else if let transcodeId = try container.decodeIfPresent(String.self, forKey: ._transcoder) {
self.transcodeId = transcodeId
}
}
}
Here, transcodeId is decided by either _transcode or _transcoder.
But I want id and rest of the keys (not included here) to be automatically decoded. How can I do it ?
You need to manually parse all the keys once you implement init(from:) in the Codable type.
struct CreatePostResponseModel: Decodable {
var transcodeId: String?
var id: String
enum CodingKeys:String, CodingKey{
case id, transcode, transcoder
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeIfPresent(String.self, forKey: .id) ?? ""
if let transcodeId = try container.decodeIfPresent(String.self, forKey: .transcode) {
self.transcodeId = transcodeId
} else if let transcodeId = try container.decodeIfPresent(String.self, forKey: .transcoder) {
self.transcodeId = transcodeId
}
}
}
In the above code,
In case you only want to decode the JSON, there is no need to use Codable. Using Decodable is enough.
Using multiple enums for CodingKey seems unnecessary here. You can use a single enum CodingKeys.
If the property name and the key name is an exact match, there is no need to explicitly specify the rawValue of that case in enum CodingKeys. So, there is no requirement of "_transcode" and "_transcoder" rawValues in TopLevelCodingKeys.
Apart from all that, you can use keyDecodingStrategy as .convertFromSnakeCase to handle underscore notation (snake case notation), i.e.
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase //here.....
let model = try decoder.decode(CreatePostResponseModel.self, from: data)
print(model)
} catch {
print(error)
}
So, you don't need to explicitly handle all the snake-case keys. It'll be handled by the JSONDecoder on its own.
This can be one of the good solution for you wherever you want you can add multiple keys for one variable:
var transcodeId:String?
public init(from decoder: Decoder) throws {
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
transcodeId = container.getValueFromAvailableKey(codingKeys: [CodingKeys._transcoder,CodingKeys._transcode])
} catch {
print("Error reading config file: \(error.localizedDescription)")
}
}
extension KeyedDecodingContainerProtocol{
func getValueFromAvailableKey(codingKeys:[CodingKey])-> String?{
for key in codingKeys{
for keyPath in self.allKeys{
if key.stringValue == keyPath.stringValue{
do{
return try self.decodeIfPresent(String.self, forKey: keyPath)
} catch {
return nil
}
}
}
}
return nil
}
}
Hope it helps.
The compiler-generated init(from:) is all-or-nothing. You can’t have it decode some keys and “manually” decode others.
One way to use the compiler-generated init(from:) is by giving your struct both of the possible encoded properties, and make transcodeId a computed property:
struct CreatePostResponseModel: Codable {
var transcodeId: String? {
get { _transcode ?? _transcoder }
set { _transcode = newValue; _transcoder = nil }
}
var _transcode: String? = nil
var _transcoder: String? = nil
var id: String = “”
// other properties
}

How to do custom transformations using Codable?

Let's say I need to transform a date string I received from a web service to a Date object.
Using ObjectMapper, that was easy:
class Example: Mappable {
var date: Date?
required init?(map: Map) { }
func mapping(map: Map) {
date <- (map["date_of_interest"], GenericTransform().dateTransform)
}
}
I just had to implement a tranformer ("GenericTransform" in this case) for date, and pass it as an argument along with the key name to decode.
Now, using Codable:
class Example2: Codable {
var name: String
var age: Int
var date: Date?
enum CodingKeys: String, CodingKey {
case name, age
case date = "date_of_interest"
}
}
To transform a date, in my understanding, I'd have to either:
1) Pass a dateDecodingStrategy to my JSONDecoder, which I don't want to, because I'm trying to keep that part of the code as a generic function.
or
2) Implement an init(from decoder: Decoder) inside Example2, which I also don't want to, because of the boilerplate code I'd have to write to decode all the other properties (which would be automatically generated otherwise):
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
let dateString = try container.decode(String.self, forKey: .date)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
self.date = date
} else {
//throw error
}
}
My question is: is there an easier way to do it than options 1 and 2 above?
Maybe tweaking the CodingKeys enum somehow?
EDIT:
The problem is not only about dates, actually. In this project that I'm working on, there are many custom transformations being done using TransformOf<ObjectType, JSONType> from ObjectMapper.
For example, a color transformation of a hex code received from a web service into a UIColor is done using this bit of code:
let colorTransform = TransformOf<UIColor, String>(fromJSON: { (value) -> UIColor? in
if let value = value {
return UIColor().hexStringToUIColor(hex: value)
}
return nil
}, toJSON: { _ in
return nil
})
I'm trying to remove ObjectMapper from the project, making these same transformations using Codable, so only using a custom dateDecodingStrategy will not suffice.
How would you guys do it? Implement a custom init(from decoder: Decoder) for every class that has to decode, for example, a color hex code?
Using dateDecodingStrategy in your case (as you only reference a single date format) is very simple…
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
We can use custom method for example like decodeAll here. Try in playground.
struct Model: Codable {
var age: Int?
var name: String?
enum CodingKeys: String, CodingKey {
case name
case age
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeAll(String.self, forKey: .name)
age = try container.decodeAll(Int.self, forKey: .age)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try? container.encode(name, forKey: .name)
try? container.encode(age, forKey: .age)
}
}
extension KeyedDecodingContainer where K: CodingKey, K: CustomDebugStringConvertible {
func decodeAll<T: Decodable>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T {
if let obj = try? decode(T.self, forKey: key) {
return obj
} else {
if type == String.self {
if let obj = try? decode(Int.self, forKey: key), let val = String(obj) as? T {
return val
} else if let obj = try? decode(Double.self, forKey: key), let val = String(obj) as? T {
return val
}
} else if type == Int.self {
if let obj = try? decode(String.self, forKey: key), let val = Int(obj) as? T {
return val
} else if let obj = try? decode(Double.self, forKey: key), let val = Int(obj) as? T {
return val
}
} else if type == Double.self {
if let obj = try? decode(String.self, forKey: key), let val = Double(obj) as? T {
return val
} else if let obj = try? decode(Int.self, forKey: key), let val = Double(obj) as? T {
return val
}
}
}
throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Wrong type for: \(key.stringValue)"))
}
}
let json = ##"{ "age": "5", "name": 98 }"##
do {
let obj = try JSONDecoder().decode(Model.self, from: json.data(using: .utf8)!)
print(obj)
} catch {
print(error)
}

Resources