Swift Codable protocol with recursive enums to encode JSON - json

I'm new to Swift Codable and I trying to encode a "complex" JSON structure from myself. It is a little difficult, because it needs to be recursive.
I found an solution on stack overflow for revusive enums, but it I fail to implement it for my JSON in Swift. (Swift Codable protocol with recursive enums) I got a solution for python but in Swift is much more complicated.
The JSON file would be something like this:
[
{
"id": 0,
"name": "Simple Rule A",
"operations": {
"attribute": "C",
"value": false
}
},
{
"id": 1,
"name": "Simple Rule A",
"operations": {
"ruleOperator": "AND",
"ruleOperand":[
{
"attribute": "A",
"value": false
},
{
"attribute": "C",
"value": false
}
]
}
},
{
"id": 2,
"name": "Simple Rule B",
"operations": {
"ruleOperator": "AND",
"ruleOperand":[
{"ruleOperator": "OR",
"ruleOperand":[
{
"attribute": "A",
"value": false
},
{
"attribute": "C",
"value": false
}
]
},
{
"attribute": "C",
"value": false
}
]
}
}
]
It is a array with Rules and every rule has an id and name and there operations. The operations can be a "node" with an Operator and Operands or "leafs" as an Operand with attribute and value.
Thats what I got:
import Foundation
struct Rule: Decodable {
let id: Int
let name: String
let operations: Operations
}
struct Operations {
//var ruleOperator: String
var ruleOperator: String?
var kind: Kind?
enum Kind {
case node([Operations])
case leaf(Operand)
}
init(name: String, ruleOp: String, kind: Kind) {
self.ruleOperator = ruleOp
self.kind = kind
}
}
extension Operations: Decodable {
enum CodingKeys: String, CodingKey {
case name
case ruleOperator
case nodes
case test
}
enum CodableError: Error {
case decoding(String)
case encoding(String)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let someRuleOperator = try? container.decode(String.self, forKey: .ruleOperator) {
self.ruleOperator = someRuleOperator
}
if let someOperand = try container.decodeIfPresent(Operand.self, forKey: .nodes) {
self.kind = .leaf(someOperand)
}
if let array = try? container.decode([Operations].self, forKey: .nodes) {
self.kind = .node(array)
}
return
}
}
struct Operand: Decodable {
let attribute: String
let value: Bool
}
enum Operator {
case IS
case AND
case OR
case XOR
}
The Problem is the "init(from decoder: Decoder)" part. Also the "ruleOperator: String?" as an Optional is crap.
Maybe some one can help me :) and give me a hint in the right direction.
(Also nice would be help how I can use the Operator enum instead of the string for e.g. "IS" or "AND".)
Solution
Rework the JSON structure and use this solution Swift Codable protocol with recursive enums
{
"id": 1,
"name": "Simple Rule A",
"operations": [
{
"attribute": "AND",
"value": null,
"operations": [
{
"attribute": "A",
"value": true,
"operations": []
},
{
"attribute": "B",
"value": true,
"operations": []
}
]
}
]
}

Related

Stuck Decoding Multidimensional JSON From URLSession

I have been stuck for a few days trying to decode a multidimensional JSON array from a URLSession call. This is my first project decoding JSON in SwiftUI. My attempts from reading up on methods others suggest do not seem to work.
Here is my JSON response from the server
"success": true,
"message": "Authorized",
"treeData": {
"Right": {
"G1P1": {
"Name": "John Johnson",
"ID": 387,
"SubText": "USA 2002"
},
"G2P1": {
"Name": "Tammy Johnson",
"ID": 388,
"SubText": "USA 2002"
},
"G2P2": {
"Name": "Susan Johnson",
"ID": 389,
"SubText": "USA 1955"
}
},
"Left": {
"G1P1": {
"Name": "Jane Doe",
"ID": 397,
"SubText": "USA 2002"
},
"G2P1": {
"Name": "John Doe",
"ID": 31463,
"SubText": "USA 2002"
},
"G2P2": {
"Name": "Susan Doe",
"ID": 29106,
"SubText": "USA 1958"
}
}
}
}
Here is my decode block of code
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
completion(.failure(.noData))
return
}
guard let treeResponse = try? JSONDecoder().decode([String: TreeResponse].self, from: data) else {
completion(.failure(.decodingError))
return
}
dump(treeResponse)
completion(.success("Hooray"))
}.resume()
And then here are my structs, which is the part I can't seem to figure out
struct TreeResponse: Codable {
let success: Bool
let message: String
let treeData: [String:SideData]
}
struct SideData: Codable {
let personKey: [String:PersonInfo]
}
struct PersonInfo: Codable {
let Name: String
let ID: Int
let SubText: String
}
My hope is to be able to access the decoded data as treeResponse.Right.G1P1.Name
Could really use help moving past this
I will post this as an answer even if it cannot be one, but that way I can at least format it properly :-).
First of all you should learn to pose your questions in a manner that makes it as easy as possible for anyone to execute your code. Swift has a particularly helpful way of doing this, you can run a Playground on it. Then you should start whittling down your question to its essence which appears to be the JSON decode. JSONDecoder usually is very helpful in providing you with decent error messages on what it does not like about your JSON, but you have to print them. A suitable Playground would look as follows:
import UIKit
let jsonStr = """
{
"success": true,
"message": "Authorized",
"treeData": {
"Right": {
"G1P1": {
"Name": "John Johnson",
"ID": 387,
"SubText": "USA 2002"
},
"G2P1": {
"Name": "Tammy Johnson",
"ID": 388,
"SubText": "USA 2002"
},
"G2P2": {
"Name": "Susan Johnson",
"ID": 389,
"SubText": "USA 1955"
}
},
"Left": {
"G1P1": {
"Name": "Jane Doe",
"ID": 397,
"SubText": "USA 2002"
},
"G2P1": {
"Name": "John Doe",
"ID": 31463,
"SubText": "USA 2002"
},
"G2P2": {
"Name": "Susan Doe",
"ID": 29106,
"SubText": "USA 1958"
}
}
}
}
"""
struct TreeResponse: Codable {
let success: Bool
let message: String
let treeData: [String:SideData]
}
struct SideData: Codable {
let personKey: [String:PersonInfo]
}
struct PersonInfo: Codable {
let Name: String
let ID: Int
let SubText: String
}
let jsonData = jsonStr.data(using:.utf8)!
do {
let tree = try JSONDecoder().decode(TreeResponse.self, from: jsonData)
print(tree)
} catch {
print(tree)
}
This will yield a somewhat descriptive error message:
keyNotFound(CodingKeys(stringValue: "personKey", intValue: nil),
Swift.DecodingError.Context(codingPath:
[CodingKeys(stringValue: "treeData", intValue: nil),
_JSONKey(stringValue: "Right", intValue: nil)],
debugDescription: "No value associated with key
CodingKeys(stringValue: \"personKey\", intValue: nil)
(\"personKey\").", underlyingError: nil))
(Indentation mine and not particularly well thought out)
This starts pointing out your problems (of which you still seem to have a lot).
The first level of decode is somewhat ok, but the second level is woefully inadequate in its current form. There is no such thing as a personKey in your JSON which would be required to fit it into a simple struct. However you still might be able to coax it through some decode method.
Considering you JSON that appears to be a bad choice and you should opt for properly modelling your tree with the given Left and Right keys, although this is probably scratching the limit of what Decodable will do for you for free, so you will have to put in some more work to get this to work on a more involved example. If the keys on the following levels have any special significance you will have to put in a special decode there too.
In any way, you should definitely learn to formulate your questions better.
when our structure are not perfect to JSON so that's why get this types error and i've use JSONDecoder to retrieve the data from JSON couldn't read the data it's missing, though, such error yet get so needs to create quite perfect JSON models or create model with CodingKeys such like:
struct JSONData: Codable {
let success: Bool
let message: String
let treeData: TreeData
}
struct TreeData: Codable {
let treeDataRight, treeDataLeft: [String: Left]
enum CodingKeys: String, CodingKey {
case treeDataRight = "Right"
case treeDataLeft = "Left"
}
}
struct Left: Codable {
let name: String
let id: Int
let subText: String
enum CodingKeys: String, CodingKey {
case name = "Name"
case id = "ID"
case subText = "SubText"
}
}
For get JSON data to need JSONDecoder():
let jsonData = jsonStr.data(using:.utf8)!
do {
let tree = try JSONDecoder().decode(JSONData.self, from: jsonData)
dump(tree)
} catch {
print(error.localizedDescription)
}
Together with json, JSON model, JSONDecoder():
let jsonStr = """
{
"success": true,
"message": "Authorized",
"treeData": {
"Right": {
"G1P1": {
"Name": "John Johnson",
"ID": 387,
"SubText": "USA 2002"
},
"G2P1": {
"Name": "Tammy Johnson",
"ID": 388,
"SubText": "USA 2002"
},
"G2P2": {
"Name": "Susan Johnson",
"ID": 389,
"SubText": "USA 1955"
}
},
"Left": {
"G1P1": {
"Name": "Jane Doe",
"ID": 397,
"SubText": "USA 2002"
},
"G2P1": {
"Name": "John Doe",
"ID": 31463,
"SubText": "USA 2002"
},
"G2P2": {
"Name": "Susan Doe",
"ID": 29106,
"SubText": "USA 1958"
}
}
}
}
"""
struct JSONData: Codable {
let success: Bool
let message: String
let treeData: TreeData
}
struct TreeData: Codable {
let treeDataRight, treeDataLeft: [String: Left]
enum CodingKeys: String, CodingKey {
case treeDataRight = "Right"
case treeDataLeft = "Left"
}
}
struct Left: Codable {
let name: String
let id: Int
let subText: String
enum CodingKeys: String, CodingKey {
case name = "Name"
case id = "ID"
case subText = "SubText"
}
}
let jsonData = jsonStr.data(using:.utf8)!
do {
let tree = try JSONDecoder().decode(JSONData.self, from: jsonData)
dump(tree)
} catch {
print(error.localizedDescription)
}
Result:
Result here
and i hope this would work and helpfully so try once

Decoding MultiType Array and Unknown Length Array within JSON using Swift

I've seen questions like this asked before on Stack Overflow, however none match this complexity. When I attempt to apply the same principles I've seen in similar StackOverflow questions, I get stuck. I'm new to using JSONs in Swift, and certainly understand how to decode fairly complex JSONs. However, this is just beyond my reach.
{
"def": [
{
"sseq": [
[
[
"pseq",
[
[
"sense",
{
"sn": "1 a (1)",
"dt": [
[
"text",
"{bc}an extremely young child"
]
],
"sdsense": {
"sd": "especially",
"dt": [
[
"text",
"{bc}{sx|infant||}"
]
]
}
}
],
[
"sense",
{
"sn": "(2)",
"dt": [
[
"text",
"{bc}an extremely young animal"
]
]
}
]
]
],
[
"sense",
{
"sn": "b",
"dt": [
[
"text",
"{bc}the youngest of a group "
],
[
"vis",
[
{
"t": "He is the {wi}baby{/wi} of the family."
}
]
]
]
}
]
],
[
[
"sense",
{
"sn": "2 a",
"dt": [
[
"text",
"{bc}one that is like a baby (as in behavior) "
],
[
"vis",
[
{
"t": "When it comes to getting shots, I'm a real {wi}baby{/wi}."
}
]
]
]
}
],
[
"sense",
{
"sn": "b",
"dt": [
[
"text",
"{bc}something that is one's special responsibility, achievement, or interest "
],
[
"vis",
[
{
"t": "The project was his {wi}baby{/wi}."
}
]
]
]
}
]
],
[
[
"sen",
{
"sn": "3",
"sls": [
"slang"
]
}
],
[
"sense",
{
"sn": "a",
"dt": [
[
"text",
"{bc}{sx|girl||}, {sx|woman||} "
],
[
"uns",
[
[
[
"text",
"often used in address"
]
]
]
]
]
}
],
[
"sense",
{
"sn": "b",
"dt": [
[
"text",
"{bc}{sx|boy||}, {sx|man||} "
],
[
"uns",
[
[
[
"text",
"often used in address "
],
[
"vis",
[
{
"t": "Hey {wi}baby{/wi}, nice car!"
}
]
]
]
]
]
]
}
]
],
[
[
"sense",
{
"sn": "4",
"dt": [
[
"text",
"{bc}{sx|person||}, {sx|thing||} "
],
[
"vis",
[
{
"t": "is one tough {wi}baby{/wi}"
}
]
]
]
}
]
]
]
}
]
}
To clarify, I only want the object that contains the "sn" and "dt" properties and looks like the following:
{
"sn":"b",
"dt":[
[
"text",
"{bc}the youngest of a group "
],
[
"vis",
[
{
"t":"He is the {wi}baby{\/wi} of the family."
}
]
]
]
}
What makes this so complicated is that:
The first "sseq" (for example) contains a 5D array that combines strings with more arrays within the same array, which happens at multiple levels.
Sometimes, it is unknown how many arrays I have to decode to get to that level.
Any help on this is appreciated!
The below function will generate an array with all values that has any of the given keys
func extract(_ keys: [String], from value: Any, to result: inout [String, Any)]) {
if let dictionary = value as? [String: Any] {
var anyFound = false
for key in keys {
if let value = dictionary[key] {
result.append((key, value))
anyFound = true
}
}
if !anyFound {
for item in dictionary {
extract(keys, from: item.value, to: &result)
}
}
} else if let array = value as? [Any] {
for item in array {
extract(keys, from: item, to: &result)
}
}
}
and you can use it like this
var array = [(String:Any)]()
do {
if let result = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
extract(["dt", "sn"], from: result, to: &array)
}
} catch {
print(error)
}
Then it is only a matter of decoding/converting the content of the array.
For completeness-sake, another approach to make this work is to encapsulate the types of values that a JSON could hold as an enum with associated values:
enum Json {
case obj([String: Json])
case arr([Json])
case str(String)
case int(Int)
case num(Decimal)
case bool(Bool)
case null
}
Then you could make it Decodable by trying to decode either as a dictionary, failing that - as an array, and finally, failing that, as a primitive value:
extension Json: Decodable {
// Need this to decode arbitrary keys
struct DynamicKeys: CodingKey {
var stringValue: String
var intValue: Int? = nil
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
// try to decode as a dictionary
if let container = try? decoder.container(keyedBy: DynamicKeys.self) {
var dict: [String: Json] = [:]
for key in container.allKeys {
let jsonValue = try container.decode(Json.self, forKey: key)
dict[key.stringValue] = jsonValue
}
self = .obj(dict)
// try to decode as an array
} else if var container = try? decoder.unkeyedContainer() {
var array: [Json] = []
while !container.isAtEnd {
let jsonValue = try container.decode(Json.self)
array.append(jsonValue)
}
self = .arr(array)
// try to decode a primitive
} else if let container = try? decoder.singleValueContainer() {
if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let decimal = try? container.decode(Decimal.self) {
self = .num(decimal)
} else if let str = try? container.decode(String.self) {
self = .str(str)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if container.decodeNil() {
self = .null
} else {
fatalError("this shouldn't have happened")
}
} else {
fatalError("this shouldn't have happened")
}
}
}
You could provide some convenience subscripts to access an array or a dictionary:
extension Json {
subscript(index: Int) -> Json? {
if case .array(let array) { return array[index] }
else { return nil }
}
subscript(index: String) -> Json? {
if case .obj(let dict) { return dict[index] }
else { return nil }
}
}
Then you could decode it normally, and you'd get an enum of possibly more enums as a result:
let json = try JSONDecoder().decode(Json.self, jsonData)
let innerJson = json["foo"]?[0]?["bar"]

How to parse JSON in Swift with dynamic filename using Codable

I am trying to parse the following JSON into a class, but don't know how to approach this particular case.
Here is the api: https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&indexpageids&titles=bird
I am trying to get the title and extract, but in order to do so, it requires that I go through the unique pageid. How would I do this using the Codable protocol?
{
"batchcomplete": "",
"query": {
"normalized": [
{
"from": "bird",
"to": "Bird"
}
],
"pageids": [
"3410"
],
"pages": {
"3410": {
"pageid": 3410,
"ns": 0,
"title": "Bird",
"extract": "..."
}
}
}
}
My suggestion is to write a custom initializer:
Decode pages as [String:Page] dictionary and map the inner dictionaries according to the values in pageids
let jsonString = """
{
"batchcomplete": "",
"query": {
"normalized": [
{
"from": "bird",
"to": "Bird"
}
],
"pageids": [
"3410"
],
"pages": {
"3410": {
"pageid": 3410,
"ns": 0,
"title": "Bird",
"extract": "..."
}
}
}
}
"""
struct Root : Decodable {
let query : Query
}
struct Query : Decodable {
let pageids : [String]
let pages : [Page]
private enum CodingKeys : String, CodingKey { case pageids, pages }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.pageids = try container.decode([String].self, forKey: .pageids)
let pagesData = try container.decode([String:Page].self, forKey: .pages)
self.pages = self.pageids.compactMap{ pagesData[$0] }
}
}
struct Page : Decodable {
let pageid, ns : Int
let title, extract : String
}
let data = Data(jsonString.utf8)
do {
let result = try JSONDecoder().decode(Root.self, from: data)
print(result)
} catch {
print(error)
}

How do you parse recursive JSON in Swift?

I am receiving JSON from a server where the data is recursive. What is the best way to parse this into a convenient Swift data structure?
Defining a Swift Codable data structure to parse it into fails because the recursive properties are not allowed.
The Swift compiler reports: "Value type 'FamilyTree.Person' cannot have a stored property that recursively contains it"
{
"familyTree": {
"rootPerson": {
"name": "Tom",
"parents": {
"mother": {
"name": "Ma",
"parents": {
"mother": {
"name": "GraMa",
"parents": {}
},
"father": {
"name": "GraPa",
"parents": {}
}
}
},
"father": {
"name": "Pa",
"parents": {}
}
}
}
}
}
Ideally the end result would be a bunch of person objects pointing to their mother and father objects starting from a rootPerson object.
The first thought is to create structs such as:
struct Person: Codable {
var name: String
var parents: Parents
}
struct Parents: Codable {
var mother: Person?
var father: Person?
}
But this doesn't work since you can't have recursive stored properties like this.
Here is one possible working solution:
let json = """
{
"familyTree": {
"rootPerson": {
"name": "Tom",
"parents": {
"mother": {
"name": "Ma",
"parents": {
"mother": {
"name": "GraMa",
"parents": {}
},
"father": {
"name": "GraPa",
"parents": {}
}
}
},
"father": {
"name": "Pa",
"parents": {}
}
}
}
}
}
"""
struct Person: Codable {
var name: String
var parents: [String: Person]
}
struct FamilyTree: Codable {
var rootPerson: Person
}
struct Root: Codable {
var familyTree: FamilyTree
}
let decoder = JSONDecoder()
let tree = try decoder.decode(Root.self, from: json.data(using: .utf8)!)
print(tree)
In a playground this will correctly parse the JSON.
The parents dictionary of Person will have keys such as "mother" and "father". This supports a person have any number of parents with any role.
Possible implementation using classes.
(Swift 5 synthesizes default initializers for classes. Dont remember if so for Swift 4)
import Foundation
var json: String = """
{
"familyTree": {
"rootPerson": {
"name": "Tom",
"parents": {
"mother": {
"name": "Ma",
"parents": {
"mother": {
"name": "GraMa",
"parents": {}
},
"father": {
"name": "GraPa",
"parents": {}
}
}
},
"father": {
"name": "Pa",
"parents": {}
}
}
}
}
}
"""
final class Parents: Codable{
let mother: Person?
let father: Person?
}
final class Person: Codable{
let name: String
let parents: Parents?
}
final class RootPerson: Codable {
var rootPerson: Person
}
final class Root: Codable {
var familyTree: RootPerson
}
var jsonData = json.data(using: .utf8)!
do{
let familyTree = try JSONDecoder().decode(Root.self, from: jsonData)
print("Ma •••>", familyTree.familyTree.rootPerson.parents?.mother?.name)
print("GraPa •••>", familyTree.familyTree.rootPerson.parents?.mother?.parents?.father?.name)
print("Shoud be empty •••>", familyTree.familyTree.rootPerson.parents?.mother?.parents?.father?.parents?.father?.name)
} catch {
print(error)
}

Parsing nested JSON using Decodable's init(from decoder:)

I'm decoding JSON using Decodable with init(from decoder:) but unable to parse out my nested JSON.
The JSON I'm trying to parse:
The main conflict I believe is parsing the array of edges.
The goal is to get a model I can use with key, value, and namespace.
Would need to use init(from decoder:).
{
"data": {
"collectionByHandle": {
"metafields": {
"edges": [
{
"node": {
"key": "city",
"value": "Fayetteville ",
"namespace": "shipping"
}
},
{
"node": {
"key": "country",
"value": "United States",
"namespace": "shipping"
}
},
{
"node": {
"key": "state",
"value": "AR",
"namespace": "shipping"
}
}
]
}
}
},
"errors": null
}
This technically works but wanting to use init(from decoder: ) and return a model that just has key, value, and namespace without having multiple separate structs/classes.
struct Shipping: Codable {
let data: DataClass
let errors: String?
}
struct DataClass: Codable {
let collectionByHandle: CollectionByHandle
}
struct CollectionByHandle: Codable {
let metafields: Metafields
}
struct Metafields: Codable {
let edges: [Edge]
}
struct Edge: Codable {
let node: Node
}
struct Node: Codable {
let key, value, namespace: String
}