Flattened Objects in JSON File to Nested Object Structure in Swift

Flattened objects in JSON file to nested object structure in Swift

You can use a custom decoder so that you don't have to create the other objects.

struct User: Decodable {
let name: String
let age: Int
let address: Address

struct Address: Decodable {
let city: String
let country: String
}

enum CodingKeys: String, CodingKey {
case user
case address
}

enum UserKeys: String, CodingKey {
case name
case age
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let user = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
name = try user.decode(String.self, forKey: .name)
age = try user.decode(Int.self, forKey: .age)
address = try container.decode(Address.self, forKey: .address)

}
}

So putting your data into a playground

let data = """
{
"users":[
{
"user":{
"name":"Adam",
"age":25
},
"address":{
"city":"Stockholm",
"country":"Sweden"
}
},
{
"user":{
"name":"Lilly",
"age":24
},
"address":{
"city":"Copenhagen",
"country":"Denmark"
}
}
]
}
""".data(using: .utf8)!

You can decode like this:

let decoder = JSONDecoder()
let result = try! decoder.decode([String:[User]].self, from: data)

Or you can create a Users struct so that you don't have to deal with dictionaries

struct Users: Decodable {
let users: [User]
}

let decoder = JSONDecoder()
let result = try! decoder.decode(Users.self, from: data)

How to decode a nested JSON struct with Swift Decodable protocol?

Another approach is to create an intermediate model that closely matches the JSON (with the help of a tool like quicktype.io), let Swift generate the methods to decode it, and then pick off the pieces that you want in your final data model:

// snake_case to match the JSON and hence no need to write CodingKey enums
fileprivate struct RawServerResponse: Decodable {
struct User: Decodable {
var user_name: String
var real_info: UserRealInfo
}

struct UserRealInfo: Decodable {
var full_name: String
}

struct Review: Decodable {
var count: Int
}

var id: Int
var user: User
var reviews_count: [Review]
}

struct ServerResponse: Decodable {
var id: String
var username: String
var fullName: String
var reviewCount: Int

init(from decoder: Decoder) throws {
let rawResponse = try RawServerResponse(from: decoder)

// Now you can pick items that are important to your data model,
// conveniently decoded into a Swift structure
id = String(rawResponse.id)
username = rawResponse.user.user_name
fullName = rawResponse.user.real_info.full_name
reviewCount = rawResponse.reviews_count.first!.count
}
}

This also allows you to easily iterate through reviews_count, should it contain more than 1 value in the future.

Is there a sane way to parse a nested JSON with unknown structure into an object or a dictionary in Swift 5?

If you are looking for an external library that does this, you can try SwiftyJSON, which allows you to get all the interests like this:

    let json = JSON(parseJSON: jsonString)
let matchesObject = json["matches"]
// I assume the total number of pages is stored in numberOfPages
let interests = (0..<2).flatMap {
matchesObject["page\($0 + 1)"].arrayValue.flatMap {
$0["interests"].arrayValue.map {
// using the same Interest struct as in your question
Interest(id: $0["id"].intValue, text: $0["text"].stringValue)
}
}
}

swift: How can I decode an array of json objects, without creating a struct, that holds an array of said objects?

If you find yourself needing this multiple times, then you can build your own generic struct that decodes over whichever key it finds:

struct Nester<T: Decodable>: Decodable {
let elements: [T]

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let key = container.allKeys.first {
elements = try container.decode([T].self, forKey: key)
} else {
// we run into an empty dictionary, let's signal this
throw DecodingError.typeMismatch([String:Any].self, DecodingError.Context(codingPath: [], debugDescription: "Expected to find at least one key"))
}
}

// A coding key that accepts whatever string value it is given
struct CodingKeys: CodingKey {
let stringValue: String
var intValue: Int? { nil }

init?(stringValue: String) {
self.stringValue = stringValue
}

init?(intValue: Int) { return nil }
}
}

And with this in hand, you can extend JSONDecoder in order to get a nicer call site:

extension JSONDecoder {
func decode<T: Decodable>(nested: [T].Type, from data: Data) throws -> [T] {
try decode(Nester<T>.self, from: data).elements
}
}

Then it's just a matter of calling the new overload:

let places = try JSONDecoder().decode(nested: [Place].self, from: data)

P.S. if you want, you can hide the complex struct within the extension, resulting in something like this:

extension JSONDecoder {
func decode<T: Decodable>(nested: [T].Type, from data: Data) throws -> [T] {
try decode(Nester<T>.self, from: data).elements
}

private struct Nester<T: Decodable>: Decodable {
let elements: [T]

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let key = container.allKeys.first {
elements = try container.decode([T].self, forKey: key)
} else {
throw DecodingError.typeMismatch([String:Any].self, DecodingError.Context(codingPath: [], debugDescription: "Expected to find at least one key"))
}
}

struct CodingKeys: CodingKey {
let stringValue: String
var intValue: Int? { nil }

init?(stringValue: String) {
self.stringValue = stringValue
}

init?(intValue: Int) { return nil }
}
}
}

The downside is that you'll not be able to reuse the struct if you want to extend other decoders besides the JSON one.

Filter a nested arrays of structs and return the child object in swift

You can use flatMap first to get all MenuRow objects in one array and then find the correct object in that array using first(where:)

let menuRow = menuSections.flatMap(\.menuRows).first(where: { $0.id == 1 })

Why and when do we need to flatten JSON objects?

There are many situations where you get JSON text that was automatically built by some library. Throughout the programming languages, there are many libraries that build JSON text (one example is here).

Whenever libraries add some additional object or array wrappings, you might want to get rid of them maybe because you send the JSON to the server and your code there crashes because it expects a primitive value instead of an object (or an array). Or, if your JSON is a server response, you don't want the resulting Javascript code having to differ between object/array or not object/array. In all these cases, flattening is helpful as it will save you time. You will have to implement lesser if/elses, and you can reliably expect your data structure to be as flat as possible.

The other approach to improve code for the scenario mentioned is to write the code in a maximal robust way so there is no way for it to crash by superfluous wrappings ever. So always expect some wrappers and get it's contents. Then, flattening is not needed.

You see, it depends on what is building the JSON and what is parsing it. The building may be out of your scope.

This leads also to data model questions. I've worked with XML code that needed to be parsed quiet a different way if there where 0 entries of some XY, or if there were >0 entries of some XY. Having a wrapper that is allowed to have 0 or more entries of some XY will make live easier. These are data model desicions.

In all cases where the JSON represents an object structure that I've combined manually, I expect it not to change. So flattening something I've designed in detail would be disturbing. Standard operations as far I've seen them do not need flattening (e.g. JSON.stringify(), json_encode() etc.)

Decode nested jsons swift

First when you're decoding your struct using:

JSONDecoder().decode(MagicalStruct.self, magicJson)

you're trying to extract a single object: let roomEmail: String.

However, your input JSON contains an array of objects with emails. Which means your code:

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let magicContainer = try container.nestedContainer(keyedBy: ScheduleCodingKeys.self, forKey: .value)
roomEmail = try magicContainer.decode(String.self, forKey: ScheduleCodingKeys.roomEmail)
}

tries to decode a single email and there is a collection instead (in your example with one element - that's why it may be confusing).

Also your error Expected to decode Dictionary<String, Any> but found an array instead is on the line:

let magicContainer = try container.nestedContainer(keyedBy: ScheduleCodingKeys.self, forKey: .value)

You need to decode an array:

var magicContainer = try container.nestedUnkeyedContainer(forKey: .value)

But then you have an array of objects with scheduleId and somethingEventMoreMagical keys. How do you want to assign all the values to your one let roomEmail: String variable?


You can decode a dictionary instead:

let result = try JSONDecoder().decode([String: [MagicalStruct]].self, from: magicJson)

print(result["value"]!) // prints [MagicalStruct(roomEmail: "magic@yahoo.com")]

And you can simplify your MagicalStruct:

struct MagicalStruct: Decodable {
enum CodingKeys: String, CodingKey {
case roomEmail = "scheduleId"
}

let roomEmail: String
}

Extract every value from a complex JSON into an array in Swift

You could write a recursive function that attempts to filter out error messages from its input.

Since your JSON structure is not well defined, you need to conditionally type and check your root / current object as an array or a dictionary.

The following is sample code that can be run in an XCode Playground.

func extractErrors(jsonObject: Any, errors: inout [String]) {
if let array = jsonObject as? [Any] {
array.forEach {
extractErrors(jsonObject: $0, errors: &errors)
}
}
else if let dict = jsonObject as? [String : Any] {
for key in dict.keys {
if key == "errors", let errorList = dict[key] as? [String] {
errorList.forEach { errors.append($0) }
}
else {
extractErrors(jsonObject: dict[key]!, errors: &errors)
}
}
}
}

let json = "{\"errors\":{\"children\":{\"name\":{\"errors\":[\"Error name\"]},\"lastName\":{\"errors\":[\"Error lastName\"]},\"email\":{\"errors\":[\"Error mail\"]},\"gender\":{},\"birthday\":{\"children\":{\"year\":{},\"month\":{\"errors\":[\"Error month\"]},\"day\":{}}}}}}"

if let data = json.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
{
var errors = [String]()
extractErrors(jsonObject: jsonObject, errors: &errors)
print(errors) // ["Error lastName", "Error name", "Error mail", "Error month"]

}


Related Topics



Leave a reply



Submit