Looking for help to decode irregular (for a lack of a better word) json. As an example:
[
{
"texts":
[
{
"value": "value 1"
}
],
"commentType": "someComment"
},
{
"texts":
[
{
"value": "value 2"
}
],
"commentType": "someComment2"
},
{
"texts":
[
{
"evidences":
[
{
"evidenceCode": "code 1",
},
{
"evidenceCode": "code 2",
},
{
"evidenceCode": "code 3",
},
{
"evidenceCode": "code 4",
}
],
"value": "value 3"
}
],
"commentType": "someComment3"
}
]
I can decode comment and the first two "texts":
enum CodingKeys: String, CodingKey {
case texts
case commentType
}
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .commentType)
if let texts = try container.decodeIfPresent([[String: String]].self, forKey: .texts) {
for text in texts {
if let value = text["value"] {
// add to model object
}
}
}
} catch {
print(error)
}
But I get an error for the third "texts" block:
"Expected to decode String but found an array instead."
Which I understand, since now instead of an array of [String:String], it is a mixed array of [String:[String:String] and [String:String].
How do I decode value3 from that third block?
Actually this is not complicated at all because the content of texts is the same if we treat evidences as an optional array. The below models will decode the json correctly without any custom code.
struct Result: Decodable {
var texts: [TextData]
var commentType: String
}
struct TextData: Decodable {
let evidences: [Evidence]?
let value: String
}
struct Evidence: Decodable {
let evidenceCode: String
}
Related
I'm trying to add in a support section (this was a demo that turned into something more) and thought that I could fetch a json file and add it into DisclosureGroup to the end user.
I originally thought that the issue was a network issue, but adding the file locally still caused the same problem.
When I run it in the simulator, and try to open one of the DisclosureGroup items, it doesn't open. If I they to press more the RAM usage increases but can't see a reason why it should be after the initial Bundle load into the array.
This is the data I was testing:
SupportQuestions.json
{
"sections": [
{
"title": "Section title 1",
"description": null,
"questions": [
{
"title": "Question title 1",
"response": "Answer 1"
},
{
"title": "Question title 3",
"response": "Answer 3"
}
]
},
{
"title": "Section title 2",
"description": "Section description",
"questions": [
{
"title": "Question title 4",
"response": "Answer 4"
},
{
"title": "Question title 5",
"response": "Answer 5"
},
{
"title": "Question title 6",
"response": "Answer 6"
}
]
},
{
"title": "Section title 3",
"description": "Another section description",
"questions": [
{
"title": "Question title 7",
"response": "Answer 7"
},
{
"title": "Question title 8",
"response": "Answer 8"
},
{
"title": "Question title 9",
"response": "Answer 9"
}
]
}
]
}
Then the Swift I was using in the View:
struct SettingsHelpView: View {
#State private
var suppportItems: [SupportSections.SupportCategory] = []
var body: some View {
Form {
ForEach(suppportItems) {
item in
Section {
ForEach(item.questions) {
question in
DisclosureGroup {
Text(question.response)
}
label: {
Text(question.title).bold()
}
}
}
header: {
Text(item.title)
}
footer: {
Text(item.decription ?? "")
}
}
}
.onAppear {
fetchHelpSection()
}
}
private func fetchHelpSection() {
let questions = Bundle.main.decode(SupportSections.self, from: "SupportQuestions.json")
suppportItems = questions.sections
}
}
Model
struct SupportSections: Decodable {
let sections: [SupportCategory]
struct SupportCategory: Decodable, Identifiable {
var id: String { UUID().uuidString }
let title: String
let decription: String?
let questions: [SupportQuestion]
struct SupportQuestion: Decodable, Identifiable {
var id: String { UUID().uuidString }
let title: String
let response: String
}
}
}
Bundle+Ext
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from file: String, dateDecodingStategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Error: Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Error: Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStategy
decoder.keyDecodingStrategy = keyDecodingStrategy
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Error: Failed to decode \(file) from bundle.")
}
return loaded
}
}
Video of what is occurring (sorry don't know how to resize):
The issue comes from your id properties in your models. Right now, you have id defined as a computed property:
var id: String { UUID().uuidString }
This means that every time SwiftUI asks for an id, it gets a different value, since a new UUID is generated each time. This confuses SwiftUI and it 'closes' the DisclosureGroup because it thinks it's a new View (because of the new ID).
To fix this, declare your id properties as non-computed values and provide CodingKeys so that the system doesn't try to decode that property from the JSON file.
struct SupportSections: Decodable {
let sections: [SupportCategory]
struct SupportCategory: Decodable, Identifiable {
var id = UUID().uuidString //<-- Here
let title: String
let description: String? //note that you had a typo here in your original code
let questions: [SupportQuestion]
enum CodingKeys : String, CodingKey {
case title, description, questions
}
struct SupportQuestion: Decodable, Identifiable {
var id: String = UUID().uuidString //<-- Here
let title: String
let response: String
enum CodingKeys : String, CodingKey {
case title, response
}
}
}
}
Let's say we have a JSON like that:
{
"id1": {
"name": "hello"
},
"id2": {
"name": "world"
}
}
A model:
struct Model: Decodable {
var id: String
var name: String
}
How is it possible to make an array of Model from the JSON above?
You could do something like this
let data = """
{
"id1": {
"name": "hello"
},
"id2": {
"name": "world"
}
}
""".data(using: .utf8)!
struct Name: Decodable {
let name: String
}
struct Model {
let id: String
let name: String
}
do {
let json = try JSONDecoder().decode([String: Name].self, from: data)
let result = json.map { Model(id: $0.key, name: $0.value.name) }
print(result)
} catch {
print(error)
}
We decode the data as [String, Name]. We could decode it as [String: [String:String]] but this will mean that we will have to handle optional values so it is easier to create a Name struct to handle that part.
Once we have the dictionary we map over it converting it into the model object, leaving an array of [Model]
Consider this example JSON:
{
"sections": [{
"title": "Sign up",
"rows": [
{
"type": "image",
"imageURL": "https://example.com/image.jpg"
},
{
"type": "textField",
"value": "",
"placeholder": "Username"
},
{
"type": "textField",
"placeholder": "password"
},
{
"type": "textField",
"placeholder": "confirmPassword"
},
{
"type": "button",
"placeholder": "Register!"
}
]
}]
}
Let's say I wanted to parse the JSON above into the following models (I know it doesn't compile due to the Row protocol not corresponding to Decodable):
enum RowType: String, Codable {
case textField
case image
case button
}
protocol Row: Codable {
var type: RowType { get }
}
struct TextFieldRow: Row {
let type: RowType
let placeholder: String
let value: String
enum CodingKey: String {
case type
case placeholder
case value
}
}
struct ImageRow: Row {
let type: RowType
let imageURL: URL
enum CodingKey: String {
case type
case imageURL
}
}
struct ButtonRow: Row {
let type: RowType
let title: String
enum CodingKey: String {
case type
case title
}
}
struct Section: Codable {
let rows: [Row]
let title: String
enum CodingKey: String {
case rows
case title
}
}
struct Response: Codable {
let sections: [Section]
enum CodingKey: String {
case sections
}
}
// Parsing the response using the Foundation JSONDecoder
let data: Data // From network
let decoder = JSONDecoder()
do {
let response = try decoder.decode(Response.self, from: data)
} catch {
print("error: \(error)")
}
Is there a way to make the Swift code above Codable compliant?
I know you can manually solve this by first grabbing each Row's type string and then creating the right type of Row model as well as changing them from structs to classes and letting the Row protocol be a superclass instead. But is there a way that requires less manual labour?
Using an enum with associated value is the best option:
Consider this enum:
enum Row: Decodable {
case textField(TextFieldRow)
case image(ImageRow)
// and other cases
case unknown
enum CodingKeys: String, CodingKey {
case type
}
public init(from decoder: Decoder) throws {
do {
let selfContainer = try decoder.singleValueContainer()
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
let type = try typeContainer.decode(String.self, forKey: .type)
switch type {
case "textField": self = .textField( try selfContainer.decode(TextFieldRow.self) )
case "Image": self = .image( try selfContainer.decode(ImageRow.self) )
// and other cases
default: self = .unknown
}
}
}
}
With these changes:
struct TextFieldRow: Decodable {
let placeholder: String?
let value: String?
}
struct ImageRow: Decodable {
let imageURL: URL
}
// and so on
Now this will decode like a charm:
// Minmal testing JSON
let json = """
[
{
"type": "image",
"imageURL": "https://example.com/image.jpg"
},
{
"type": "textField",
"value": "",
"placeholder": "Username"
},
{
"type": "textField",
"placeholder": "password"
}
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
print( try! decoder.decode([Row].self, from: json) )
You can now add any other case you need to the decoder to build your application builder app.
Codable is great when you know the key formatting of the JSON data. But what if you don't know the keys? I'm currently faced with this problem.
Normally I would expect JSON data to be returned like this:
{
"id": "<123>",
"data": [
{
"id": "<id1>",
"event": "<event_type>",
"date": "<date>"
},
{
"id": "<id2>",
"event": "<event_type>",
"date": "<date>"
},
]
}
But this is what I'm aiming to decode:
{
"id": "123",
"data": [
{ "<id1>": { "<event>": "<date>" } },
{ "<id2>": { "<event>": "<date>" } },
]
}
Question is: how do I use Codable to decode JSON where the keys are unique? I feel like I'm missing something obvious.
This is what I'm hoping to do so I can use Codable:
struct SampleModel: Codable {
let id: String
let data: [[String: [String: Any]]]
// MARK: - Decoding
enum CodingKeys: String, CodingKey {
case id = "id"
case data = "data"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
// This throws an error: Ambiguous reference to member 'decode(_:forKey:)'
data = try container.decode([[String: [String: Any]]].self, forKey: .data)
}
}
This throws an error: Ambiguous reference to member 'decode(_:forKey:)'
For your completely changed question, the solution is very similar. Your struct simply adds one additional layer above the array. There's no need for any custom decoding nor even any CodingKeys.
Note that you can't use Any in a Codable.
let json="""
{
"id": "123",
"data": [
{ "<id1>": { "<event>": "2019-05-21T16:15:34-0400" } },
{ "<id2>": { "<event>": "2019-07-01T12:15:34-0400" } },
]
}
"""
struct SampleModel: Codable {
let id: String
let data: [[String: [String: Date]]]
}
var decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let res = try decoder.decode(SampleModel.self, from: json.data(using: .utf8)!)
print(res)
} catch {
print(error)
}
The original answer for your original question.
Since you have an array of nested dictionary where none of the dictionary keys are fixed, and since there are no other fields, you can just decode this as a plain array.
Here's an example:
let json="""
[
{ "<id1>": { "<event>": "2019-07-01T12:15:34-0400" } },
{ "<id2>": { "<event>": "2019-05-21T17:15:34-0400" } },
]
"""
var decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let res = try decoder.decode([[String: [String: Date]]].self, from: json.data(using: .utf8)!)
print(res)
} catch {
print(error)
}
I'm trying to implement the new Codable protocol, so I added Codable to my struct, but am stuck on decoding the JSON.
Here's what I had before:
Struct -
struct Question {
var title: String
var answer: Int
var question: Int
}
Client -
...
guard let data = data else {
return
}
do {
self.jsonResponse = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
let questionItems = self.jsonResponse?["themes"] as! [[String: Any]]
questionItems.forEach {
let item = Question(title: $0["title"] as! String,
answer: $0["answer"] as! Int,
question: $0["question"] as! Int)
questionData.append(item)
}
} catch {
print("error")
}
Here's what I have now, except I can't figure out the decoder part:
Struct -
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
Client -
...
let decoder = JSONDecoder()
if let questions = try? decoder.decode([Question].self, from: data) {
// Can't get past this part
} else {
print("Not working")
}
It prints "Not working" because I can't get past the decoder.decode part. Any ideas? Will post any extra code as needed, thanks!
EDIT:
Sample of API JSON:
{
"themes": [
{
"answer": 1,
"question": 44438222,
"title": "How many letters are in the alphabet?"
},
{
"answer": 0,
"question": 44438489,
"title": "This is a random question"
}
]
}
If I print self.jsonResponse I get this:
Optional(["themes": <__NSArrayI 0x6180002478f0>(
{
"answer" = 7;
"question" = 7674790;
title = "This is the title of the question";
},
{
"answer_" = 2;
"question" = 23915741;
title = "This is the title of the question";
}
My new code:
struct Theme: Codable {
var themes : [Question]
}
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
...
if let decoded = try? JSONDecoder().decode(Theme.self, from: data) {
print("decoded:", decoded)
} else {
print("Not working")
}
If your JSON has a structure
{"themes" : [{"title": "Foo", "answer": 1, "question": 2},
{"title": "Bar", "answer": 3, "question": 4}]}
you need an equivalent for the themes object. Add this struct
struct Theme : Codable {
var themes : [Question]
}
Now you can decode the JSON:
if let decoded = try? JSONDecoder().decode(Theme.self, from: data) {
print("decoded:", decoded)
} else {
print("Not working")
}
The containing Question objects are decoded implicitly.
You're getting this error because your JSON is likely structured as so:
{
"themes": [
{ "title": ..., "question": ..., "answer": ... },
{ "title": ..., "question": ..., "answer": ... },
{ ... }
],
...
}
However, the code you wrote expects a [Question] at the top level. What you need is a different top-level type that has a themes property which is a [Question]. When you decode that top-level type, your [Question] will be decoded for the themes key.
Hello #all I have added the code for JSON Encoding and Decoding for Swift 4.
Please use the link here