Codable/Decodable Should Decode Array with Strings

Codable/Decodable should decode Array with Strings

You have defined names as an optional property of Country.
If your intention is that this key may not be present in the JSON
then use decodeIfPresent:

extension Country {
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
names = try values.decodeIfPresent([String].self, forKey: .names)
}
}

This method returns nil if the container does not have a value associated with key, or if the value is null.

But actually you can just omit your custom init(from decoder: Decoder)
implementation (and the enum CodingKeys), because that is the default behaviour and will
be synthesized automatically.

Remark: An implicit variable error is defined in any catch clause,
so

} catch {
print(error.localizedDescription)
}

can be more informative than just a print("error") (although not
in this particular case).

Using Swift decodable on nested arrays of strings

You just need to create the appropriate structure and pass it to the decoder:

struct Root: Decodable {
let people: [String]
let departments: [[String]]
}

let decoder = JSONDecoder()
do {
let model = try decoder.decode(Root.self, from: dataResponse)
print(model.people) // ["Alice", "Bob"]\n"
print(model.departments) // [["Accounts", "Sales"]]\n"
} catch {
print(error)
}

Custom Decodable type that can be an Array or a String

The issue here is that singleValueContainer can be used to decode also an array. So, the error that you are getting is produced by the second try inside init(from:) function of TitleStringValue and not before.

Having said that, you can further simplify your custom decoding like this:

struct TitleStringValue: Decodable {
let text: String

struct TitleStringValueInner: Decodable {
let text: String
}

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode([TitleStringValueInner].self).first?.text {
text = string
} else {
text = try container.decode(String.self)
}
}
}

Swift - Using Decodable to decode JSON array of just strings

The corresponding struct of this JSON is

struct User: Decodable {
let names: [String]
}

and decode

let model = try decoder.decode(User.self, from: dataResponse)

and get the names with

let names = model.names

or traditionally without the overhead of JSONDecoder

let model = try JSONSerialization.jsonObject(with: dataResponse) as? [String:[String]]

Hot to decode JSON data that could and array or a single element in Swift?

Your JSON actually is either a String or an array of Strings. So you need to create a custom decoder to decode and then convert them to Double:

struct Info {
let author, title: String
let tags: [Tags]
let price: [Double]
enum Tags: String, Codable {
case nonfiction, biography, fiction
}
}

extension Info: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
author = try container.decode(String.self, forKey: .author)
title = try container.decode(String.self, forKey: .title)
tags = try container.decode([Tags].self, forKey: .tags)
do {
price = try [Double(container.decode(String.self, forKey: .price)) ?? .zero]
} catch {
price = try container.decode([String].self, forKey: .price).compactMap(Double.init)
}
}
}

Playground testing

let infoData = Data("""
{
"author" : "Mark A",
"title" : "The Great Deman",
"tags" : [
"nonfiction",
"biography"
],
"price" : "242"

}
""".utf8)
do {
let info = try JSONDecoder().decode(Info.self, from: infoData)
print("price",info.price) // "price [242.0]\n"
} catch {
print(error)
}

let infoData2 = Data("""
{
"author" : "Mark A",
"title" : "The Great Deman",
"tags" : [
"nonfiction",
"biography"
],
"price" : [
"242",
"299",
"335"
]

}
""".utf8)

do {
let info = try JSONDecoder().decode(Info.self, from: infoData2)
print("price",info.price) // "price [242.0, 299.0, 335.0]\n"
} catch {
print(error)
}

Decoding an Array in a Swift model (Decodable)

import Foundation

let string = """
{
"id": "xxxxxx",
"result": [
[
"client_id",
"name",
50,
"status"
]
],
"error": null
}
"""

struct Container: Codable {
let id: String
let result: [[Result]]
let error: String?
}

enum Result: 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(Result.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Result"))
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self)
}
}

let jsonData = string.data(using: .utf8)!
let container = try? JSONDecoder().decode(Container.self, from: jsonData)

print(container)

improved @ArinDavoodian's answer.

To read the data:

container?.result.first?.forEach { object in
switch object {
case let .integer(intValue):
print(intValue)
break
case let .string(stringValue):
print(stringValue)
break
}
}

a simple solution:

let yourInsideArray = container?.result.first!
for index in 0..<yourInsideArray.count {
let yourObjectInsideThisArray = yourInsideArray[i]
//do some
switch yourObjectInsideThisArray {
case let .integer(intValue):
print(intValue)
break
case let .string(stringValue):
print(stringValue)
break
}
}

Swift 4.1 Codable/Decodable Nested Array

Given what you've described, you should store params as an enum like this:

enum Param: CustomStringConvertible {
case string(String)
case int(Int)
case array([Param])

var description: String {
switch self {
case let .string(string): return string
case let .int(int): return "\(int)"
case let .array(array): return "\(array)"
}
}
}

A param can either be a string, an int, or an array of more params.

Next, you can make Param Decodable by trying each option in turn:

extension Param: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else {
self = .array(try container.decode([Param].self))
}
}
}

Given this, there's no need for custom decoding logic in LMSRequest:

struct LMSRequest: Decodable {
let id : Int?
let method : String?
let params : [Param]?
}

As a side note, I would carefully consider whether these fields are all truly optional. It's very surprising that id is optional, and quite surprising that method is optional, and slightly surprising that params are optional. If they're not really optional, don't make them optional in the type.


From your comments, you're probably misunderstanding how to access enums. params[1] is not a [Param]. It's an .array([Param]). So you have to pattern match it since it might have been a string or an int.

if case let .array(values) = lms.params[1] { print(values[0]) }

That said, if you're doing this a lot, you can make this simpler with extensions on Param:

extension Param {
var stringValue: String? { if case let .string(value) = self { return value } else { return nil } }
var intValue: Int? { if case let .int(value) = self { return value } else { return nil } }
var arrayValue: [Param]? { if case let .array(value) = self { return value } else { return nil } }

subscript(_ index: Int) -> Param? {
return arrayValue?[index]
}
}

With that, you can say things like:

let serverstatus: String? = lms.params[1][0]?.stringValue

Which is probably closer to what you had in mind. (The : String? is just to be clear about the returned type; it's not required.)

For a more complex and worked-out example of this approach, see my generic JSON Decodable that this is a subset of.



Related Topics



Leave a reply



Submit