Swift Codable - Parse JSON Array Which Can Contain Different Data Type

Swift Codable - Parse JSON array which can contain different data type

I used quicktype to infer the type of config_data and it suggested an enum with separate cases for your object, string, and integer values:

struct ConfigData {
let configData: [ConfigDatumElement]
}

enum ConfigDatumElement {
case configDatumClass(ConfigDatumClass)
case integer(Int)
case string(String)
}

struct ConfigDatumClass {
let name, configTitle: String
}

Here's the complete code example. It's a bit tricky to decode the enum but quicktype helps you out there:

// To parse the JSON, add this file to your project and do:
//
// let configData = try? JSONDecoder().decode(ConfigData.self, from: jsonData)

import Foundation

struct ConfigData: Codable {
let configData: [ConfigDatumElement]

enum CodingKeys: String, CodingKey {
case configData = "config_data"
}
}

enum ConfigDatumElement: Codable {
case configDatumClass(ConfigDatumClass)
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
}
if let x = try? container.decode(ConfigDatumClass.self) {
self = .configDatumClass(x)
return
}
throw DecodingError.typeMismatch(ConfigDatumElement.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ConfigDatumElement"))
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .configDatumClass(let x):
try container.encode(x)
case .integer(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}

struct ConfigDatumClass: Codable {
let name, configTitle: String

enum CodingKeys: String, CodingKey {
case name
case configTitle = "config_title"
}
}

It's nice to use the enum because you get the most type-safety that way. The other answers seem to lose this.

Using quicktype's convenience initializers option, a working code sample is:

let data = try ConfigData("""
{
"config_data": [
{
"name": "illuminate",
"config_title": "Blink"
},
{
"name": "shoot",
"config_title": "Fire"
},
"illuminate",
"shoot",
25,
100
]
}
""")

for item in data.configData {
switch item {
case .configDatumClass(let d):
print("It's a class:", d)
case .integer(let i):
print("It's an int:", i)
case .string(let s):
print("It's a string:", s)
}
}

This prints:

It's a class: ConfigDatumClass(name: "illuminate", configTitle: "Blink")
It's a class: ConfigDatumClass(name: "shoot", configTitle: "Fire")
It's a string: illuminate
It's a string: shoot
It's an int: 25
It's an int: 100

Decode json array data with different datatypes in IOS using struct decodable swift 4/5

Use enums with associated values:

struct User: Codable {
let command, updated: Int
let data: [Datum]
}

enum Datum: Codable {
case double(Double)
case string(String)

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Double.self) {
self = .double(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(Datum.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Datum"))
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .double(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}

to get at the individual values in data, use code like this:

let json = """
{"command": 1, "updated": 2, "data": ["stringData1", 42, 43]}
""".data(using: .utf8)

do {
let user = try JSONDecoder().decode(User.self, from: json!)

for d in user.data {
switch d {
case .string(let str): print("String value: \(str)")
case .double(let dbl): print("Double value: \(dbl)")
}
}
} catch {
print(error)
}

How can i parse an Json array of a list of different object using Codable?

A reasonable solution is an enum with associated values because the type can be determined by the productype key. The init method first decodes the productype with a CodingKey then in a switch it decodes (from a singleValueContainer) and associates the proper type/value to the corresponding case.

enum ProductType: String, Codable {
case a, b, c
}

struct Root : Codable {
let items : [Product]
}

struct ProductA : Codable {
let id, name: String
let productype: ProductType
let propertyOfA : String
}

struct ProductB : Codable {
let id, name: String
let productype: ProductType
let propertyOfB : String
}

struct ProductC : Codable {
let id, name: String
let productype: ProductType
let propertyOfC, propertyOfC2 : String
}

enum Product : Codable {

case a(ProductA), b(ProductB), c(ProductC)

enum CodingKeys : String, CodingKey { case productype }

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ProductType.self, forKey: .productype)
let singleContainer = try decoder.singleValueContainer()
switch type {
case .a : self = .a(try singleContainer.decode(ProductA.self))
case .b : self = .b(try singleContainer.decode(ProductB.self))
case .c : self = .c(try singleContainer.decode(ProductC.self))
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let productA): try container.encode(productA)
case .b(let productB): try container.encode(productB)
case .c(let productC): try container.encode(productC)
}
}
}

And decode

let jsonString = """
{
"items": [
{
"id": "1",
"name": "name",
"propertyOfA": "1243",
"productype": "a"

},
{
"id": "2",
"name": "name",
"propertyOfA": "12",
"productype": "a"
},
{
"id": "3",
"name": "name",
"propertyOfA": "1243",
"productype": "a"
},
{
"id": "1",
"name": "name",
"propertyOfB": "1243",
"productype": "b"
},
{
"id": "1",
"name": "name",
"propertyOfC": "1243",
"propertyOfC2": "1243",
"productype": "c"
}
]
}
"""
do {
let result = try JSONDecoder().decode(Root.self, from: Data(jsonString.utf8))
print(result)
} catch { print(error)}

To read the enum values use also a switch.

Handling JSON Array Containing Multiple Types - Swift 4 Decodable

I figured out how to decode the mixed included array into two arrays of one type each. Using two Decodable structs is easier to deal with, and more versatile, than having one struct to cover multiple types of data.

This is what my final solution looks like for anyone who's interested:

struct Root: Decodable {
let data: [Post]?
let members: [Member]
let images: [ImageMedium]

init(from decoder: Decoder) throws {

let container = try decoder.container(keyedBy: CodingKeys.self)

data = try container.decode([Post].self, forKey: .data)

var includedArray = try container.nestedUnkeyedContainer(forKey: .included)
var membersArray: [Member] = []
var imagesArray: [ImageMedium] = []

while !includedArray.isAtEnd {

do {
if let member = try? includedArray.decode(Member.self) {
membersArray.append(member)
}
else if let image = try? includedArray.decode(ImageMedium.self) {
imagesArray.append(image)
}
}
}
members = membersArray
images = imagesArray
}

enum CodingKeys: String, CodingKey {
case data
case included
}
}

struct Post: Decodable {
let id: String?
let type: String?
let title: String?
let ownerId: String?
let ownerType: String?

enum CodingKeys: String, CodingKey {
case id
case type
case title
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}

struct Member: Decodable {
let id: String?
let type: String?
let firstName: String?
let lastName: String?

enum CodingKeys: String, CodingKey {
case id
case type
case firstName = "first-name"
case lastName = "last-name"
}
}

struct ImageMedium: Decodable {
let id: String?
let type: String?
let assetUrl: String?
let ownerId: String?
let ownerType: String?

enum CodingKeys: String, CodingKey {
case id
case type
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}

How to create a model in Swift from different data-type Json array?

That is some truly strange json but it can be decoded :)

First change MixedData to

struct MixedData: Codable {
let addressField: String
let someData: GetSomeData
}

and then add a custom init(from:) to RequestMyData where we use an unkeyed container for the input and output arrays and when looping those arrays we create another unkeyed container to decode to the MixedData type

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
timeUsed = try container.decode(Int.self, forKey: .timeUsed)

input = try Self.decodeMixedData(try container.nestedUnkeyedContainer(forKey: .input))
output = try Self.decodeMixedData(try container.nestedUnkeyedContainer(forKey: .output))

getSomeData = try container.decode(GetSomeData.self, forKey: .getSomeData)
}

private static func decodeMixedData(_ unkeyedContainer: UnkeyedDecodingContainer) throws -> [MixedData] {
var container = unkeyedContainer
var result = [MixedData]()
while !container.isAtEnd {
var innerContainer = try container.nestedUnkeyedContainer()
let mixed = MixedData(addressField: try innerContainer.decode(String.self),
someData: try innerContainer.decode(GetSomeData.self))
result.append(mixed)
}

return result
}

Note that the property getSomeData is not an array.

How to decode JSON Array with different objects with Codable in Swift?

What I want to achive is that one struct as a wrapper and struct for (Id, ServicePublicKey, Token etc..).
At the end when there is a new type coming from JSON, I need to write only relevant struct and add some code inside wrapper.
My Question is that: How can I parse this JSON without any optional variable?

First of all I totally agree with your idea.
When decoding a JSON we should always aim to

  • no optionals (as long as this is guaranteed by the backend)
  • easy extendibility

Let's start

So given this JSON

let data = """
{
"Response": [
{
"Id": {
"id": 123456
}
},
{
"Token": {
"token": "myToken",
"updated": "2020-01-11 13:55:43.397764",
"created": "2020-01-11 13:55:43.397764",
"id": 123456
}
},
{
"ServerPublicKey": {
"server_public_key": "some key"
}
}
]
}
""".data(using: .utf8)!

ID Model

struct ID: Decodable {
let id: Int
}

Token Model

struct Token: Decodable {
let token: String
let updated: String
let created: String
let id: Int
}

ServerPublicKey Model

struct ServerPublicKey: Decodable {
let serverPublicKey: String
enum CodingKeys: String, CodingKey {
case serverPublicKey = "server_public_key"
}
}

Result Model

struct Result: Decodable {

let response: [Response]

enum CodingKeys: String, CodingKey {
case response = "Response"
}

enum Response: Decodable {

enum DecodingError: Error {
case wrongJSON
}

case id(ID)
case token(Token)
case serverPublicKey(ServerPublicKey)

enum CodingKeys: String, CodingKey {
case id = "Id"
case token = "Token"
case serverPublicKey = "ServerPublicKey"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch container.allKeys.first {
case .id:
let value = try container.decode(ID.self, forKey: .id)
self = .id(value)
case .token:
let value = try container.decode(Token.self, forKey: .token)
self = .token(value)
case .serverPublicKey:
let value = try container.decode(ServerPublicKey.self, forKey: .serverPublicKey)
self = .serverPublicKey(value)
case .none:
throw DecodingError.wrongJSON
}
}
}
}

Let's decode!

We can finally decode your JSON

do {
let result = try JSONDecoder().decode(Result.self, from: data)
print(result)
} catch {
print(error)
}

Output

And this is the output

Result(response: [
Result.Response.id(
Result.Response.ID(
id: 123456
)
),
Result.Response.token(
Result.Response.Token(
token: "myToken",
updated: "2020-01-11 13:55:43.397764",
created: "2020-01-11 13:55:43.397764",
id: 123456)
),
Result.Response.serverPublicKey(
Result.Response.ServerPublicKey(
serverPublicKey: "some key"
)
)
])

Date Decoding

I leave the date decoding to you as homework ;-)

UPDATE

This additional part should answer to your comment

Can we store variables like id, serverPublicKey inside Result struct
without Response array. I mean instead of ResponseArray can we just
have properties? I think It need a kind of mapping but I can't figure
out.

Yes, I think we can.

We need to add one more struct to the ones already described above.

Here it is

struct AccessibleResult {

let id: ID
let token: Token
let serverPublicKey: ServerPublicKey

init?(result: Result) {

typealias ComponentsType = (id: ID?, token: Token?, serverPublicKey: ServerPublicKey?)

let components = result.response.reduce(ComponentsType(nil, nil, nil)) { (res, response) in
var res = res
switch response {
case .id(let id): res.id = id
case .token(let token): res.token = token
case .serverPublicKey(let serverPublicKey): res.serverPublicKey = serverPublicKey
}
return res
}

guard
let id = components.id,
let token = components.token,
let serverPublicKey = components.serverPublicKey
else { return nil }

self.id = id
self.token = token
self.serverPublicKey = serverPublicKey
}
}

This AccessibleResult struct has an initialiser which receives a Result value and tries to populated its 3 properties

let id: ID
let token: Token
let serverPublicKey: ServerPublicKey

If everything goes fine, I mean if the input Result contains at least an ID, a Token and a ServerPublicKey then the AccessibleResponse is initialised, otherwise the init fails and nil` is returned.

Test

if
let result = try? JSONDecoder().decode(Result.self, from: data),
let accessibleResult = AccessibleResult(result: result) {
print(accessibleResult)
}

Decode json array as an object in swift

First, I assume you'd really like the final result to be [Product] where Product looks like this:

struct Product {
var name: String = ""
var quantity: Int = 0
}

So if any keys are missing, they'll get the defaults. You could change this solution to throw an error in that case, but I think this approach is much nicer than Optionals.

Given that, here's how you decode it:

extension Product: Decodable {
init(from decoder: Decoder) throws {
// This is a very uniform type (all Strings). Swift can decode that
// without much help.
let container = try decoder
.singleValueContainer()
.decode([String: [[String: String]]].self)

// Extract the Products. Throwing an error here might be nicer.
let keyValues = container["Product"] ?? []

// This loops over every Key-Value element, and then loops through
// each one. We only expect one element in the second loop, though.
for keyValue in keyValues {
for (key, value) in keyValue {
switch key {
case "Name": self.name = value
case "Quantity": self.quantity = Int(value) ?? 0
default: break // Ignore unknown keys
}
}
}
// This solution just assigns defaults for missing keys, but
// you could validate that everything was found, and throw an
// error here if desired.
}
}

let data = try JSONDecoder().decode([Product].self, from: jsonData)
// [{name "exampleName", quantity 1}]

data.first!.name
// "exampleName"

In most cases the above is probably fine, but it's also very sloppy about malformed data. It just returns a default object, which could make an error very hard to track down. This example goes in the other direction, and does all the error checking you would expect from a normal Decodable.

First, we'll need a CodingKey that can accept any String. I really don't understand why this isn't built into stdlib:

struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral {
var stringValue: String
init(stringValue: String) { self.stringValue = stringValue }
init(_ stringValue: String) { self.init(stringValue: stringValue) }
var intValue: Int?
init?(intValue: Int) { return nil }
init(stringLiteral value: String) { self.init(value) }
}

I also find DecodingErrors very cumbersome to build, so I often build little helper functions:

func keyNotFound(_ key: String, codingPath: [CodingKey]) -> Error {
DecodingError.keyNotFound(AnyStringKey(key),
.init(codingPath: [],
debugDescription: "\(key) key not found"))
}

func typeMismatch(_ key: String, expected: Any.Type, codingPath: [CodingKey]) -> Error {
DecodingError.typeMismatch(expected, .init(codingPath: codingPath + [AnyStringKey(key)],
debugDescription: "Expected \(expected)."))
}

With those in place, here's a more strict decoder:

extension Product: Decodable {
init(from decoder: Decoder) throws {
// This is a very uniform type (all Strings). Swift can decode that
// without much help.
let container = try decoder
.singleValueContainer()
.decode([String: [[String: String]]].self)

var codingPath: [AnyStringKey] = []

// Extract the Products. Throwing an error here might be nicer.
guard let keyValues = container["Product"] else {
throw keyNotFound("Product", codingPath: codingPath)
}

codingPath.append("Product")

var name: String?
var quantity: Int?

// This loops over every Key-Value element, and then loops through
// each one. We only expect one element in the second loop, though.
for keyValue in keyValues {
for (key, value) in keyValue {
switch key {
case "Name":
name = value

case "Quantity":
guard let intValue = Int(value) else {
throw typeMismatch("Quantity",
expected: Int.self,
codingPath: codingPath)
}
quantity = intValue

default: break // Ignore unknown keys
}
}
}

guard let name = name else {
throw keyNotFound("Name", codingPath: codingPath)
}
self.name = name

guard let quantity = quantity else {
throw keyNotFound("Quantity", codingPath: codingPath)
}
self.quantity = quantity
}
}

Extracting data from JSON array with swift Codable

I cannot use [Any] here.

Never use Any when decoding JSON because usually you do know the type of the contents.

To decode an array you have to use an unkeyedContainer and decode the values in series

struct PortfolioResponseModel: Decodable {
var dataset: Dataset

struct Dataset: Decodable {
var data: [PortfolioData]

struct PortfolioData: Decodable {
let date : String
let value : Double

init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
date = try container.decode(String.self)
value = try container.decode(Double.self)
}
}
}
}

You can even decode the date strings as Date

struct PortfolioData: Decodable {
let date : Date
let value : Double

init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
date = try container.decode(Date.self)
value = try container.decode(Double.self)
}
}

if you add a date formatter to the decoder

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
let details2 = try decoder.decode(PortfolioResponseModel.self, from: Data(myJSONArray.utf8))


Related Topics



Leave a reply



Submit