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

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

Related

App crashes when setting custom Struct value in AppStorage

I have a custom struct I want to store in AppStorage:
struct Budget: Codable, RawRepresentable {
enum CodingKeys: String, CodingKey {
case total, spent
}
var total: Double
var spent: Double
init(total: Double = 5000.0, spent: Double = 3000.0) {
self.total = total
self.spent = spent
}
init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Budget.self, from: data)
else { return nil }
self = result
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return ""
}
return result
}
}
I then have the following view:
struct DemoView: View {
#AppStorage(UserDefaults.StorageKeys.budget.rawValue) var budget = Budget()
var body: some View {
Button("Update") {
budget.total = 10
}
}
}
When I tap the button the app crashes with Thread 1: EXC_BAD_ACCESS on guard let data = try? JSONEncoder().encode(self) for rawValue in Budget. What am I doing wrong here?
You are running into infinite recursion. This is because types that conforms to both Encodable and RawRepresentable automatically get this encode(to:) implementation (source), which encodes the raw value. This means that when you call JSONEncoder().encode, it would try to call the getter of rawValue, which calls JSONEncoder().encode, forming infinite recursion.
To solve this, you can implement encode(to:) explicitly:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(total, forKey: .total)
try container.encode(spent, forKey: .spent)
}
Note that you should also implement init(from:) explicitly, because you also get a init(from:) implementation (source) that tries to decode your JSON as a single JSON string, which you certainly do not want.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
total = try container.decode(Double.self, forKey: .total)
spent = try container.decode(Double.self, forKey: .spent)
}

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

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.

Transferring data from json model to array

Is it possible to keep both int and string values ​​in an array? I need help with this.I pulled the data from JSON API. But I couldn't transfer some variables to arrays.
My model is :
struct Input: Codable {
let name: String
let species: Species
let gender: Gender
let house, dateOfBirth: String
let yearOfBirth: YearOfBirth
let ancestry, eyeColour, hairColour: String
let wand: Wand
let patronus: String
let hogwartsStudent, hogwartsStaff: Bool
let actor: String
let alive: Bool
let image: String
}
enum YearOfBirth: Codable {
case integer(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Int.self) {
self = .integer(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(YearOfBirth.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for YearOfBirth"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .integer(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}
When I get the yearOfBirth to a String or Integer array, it given error Cannot convert value of type 'YearOfBirth' to expected argument type 'String'
Printing yearOfBirth :
integer(1980)
integer(1979)
integer(1980)
integer(1980)
integer(1925)
integer(1977)
string("")
integer(1960)
You are keeping both Int and String values ​​in an array.
To retrieve them, assuming you have let years = [YearOfBirth]:
for year in years {
switch year {
case .string(let text): print(text) // You can handle texts here
case .integer(let number): print(number) // You can handle numbers here
}
}
Also, you can convert all to an array of integers like:
let numbers: [Int] = years.compactMap { if case .integer(let number) = $0 { return number } else { return nil } }
As the value for yearOfBirth is only Int or empty String I'd recommend to use a wrapper struct rather than an enum with associated types and declare the value as optional Int
struct YearOfBirth: Codable {
let year : Int?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let integerValue = try? container.decode(Int.self) {
year = integerValue
} else if let stringValue = try? container.decode(String.self) {
year = Int(stringValue)
} else {
throw DecodingError.typeMismatch(YearOfBirth.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for YearOfBirth"))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(year)
}
}
I would ask the owner of the service to refactor the design to send consistent data. Sending an empty string as integer-nil-value is ridiculous.

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
}

Swift codable, Default Value to Class property when key missing in the JSON

As you know Codable is new stuff in swift 4, So we gonna move to this one from the older initialisation process for the Models. Usually we use the following Scenario
class LoginModal
{
let cashierType: NSNumber
let status: NSNumber
init(_ json: JSON)
{
let keys = Constants.LoginModal()
cashierType = json[keys.cashierType].number ?? 0
status = json[keys.status].number ?? 0
}
}
In the JSON cashierType Key may missing, so we giving the default Value as 0
Now while doing this with Codable is quite easy, as following
class LoginModal: Coadable
{
let cashierType: NSNumber
let status: NSNumber
}
as mentioned above keys may missing, but we don't want the Model Variables as optional, So How we can achieve this with Codable.
Thanks
Use init(from decoder: Decoder) to set the default values in your model.
struct LoginModal: Codable {
let cashierType: Int
let status: Int
enum CodingKeys: String, CodingKey {
case cashierType = "cashierType"
case status = "status"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.cashierType = try container.decodeIfPresent(Int.self, forKey: .cashierType) ?? 0
self.status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0
}
}
Data Reading:
do {
let data = //JSON Data from API
let jsonData = try JSONDecoder().decode(LoginModal.self, from: data)
print("\(jsonData.status) \(jsonData.cashierType)")
} catch let error {
print(error.localizedDescription)
}

Resources