Decode nested JSON child with Decodable [duplicate] - json

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
The new Swift "Decoder" class sounds like a great way to parse JSON data, but all of the examples I've found use a well-known, well-defined 'struct' to do so.
In my case I'm querying an arbitrary website that returns a HUGE JSON string and I only care about a few of the (deeply nested) fields, so I don't want to take all that time to define a 'struct' to get at them.
Is it even possible to do this with "Decoder"? And if so, how does one go about it?

The question seems to be based on a misapprehension about how Decodable works. As a convenience, Decodable is willing to do some automatic code generation behind the scenes so that you can define a struct or nest of structs and just decode the entirety of the JSON. But you are not required to take advantage of that in order to decode JSON.
There is no need to define struct properties for "fields" you don't care about. If a JSON dictionary contains 100 keys and your corresponding struct contains just one property, no problem; that key will be fetched, and no others.
With regard to the "deeply nested" part, it should not take you much time to write simple nested structs that perform the dive to reach the dictionary you really care about. But if you don't want to do even that, you could write an implementation of init(from:) that dives down and fetches out the desired values.
In other words, if you think of Decodable as consisting primarily of your implementation of init(from:), and learn to write the code that it needs, you will see that this JSON can be parsed in a few quick simple lines of code.
As an example, here's a JSON sketch of a deeply nested piece of information with a bunch of extra information at every level that we want to ignore:
{
"ignore": true,
"outer1": {
"ignore": true,
"outer2": {
"ignore": true,
"outer3": {
"name": "matt",
"ignore": true
}
}
}
}
What I'd like to do is define a very simple struct Person that consists solely of the deeply nested name:
struct Person : Decodable {
let name : String
}
I can do that! To do so, I implement Decodable myself, supplying a "hoover" CodingKey adopter struct and an implementation of init(from:), like this (this may look like a lot of work, but it isn't, because the AnyCodingKey implementation is boilerplate, copied and pasted from here, and the init(coder:) implementation is just a few lines of code that were easy to write):
struct Person : Decodable {
let name : String
struct AnyCodingKey : CodingKey {
var stringValue: String
var intValue: Int?
init(_ codingKey: CodingKey) {
self.stringValue = codingKey.stringValue
self.intValue = codingKey.intValue
}
init(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
init(from decoder: Decoder) throws {
var con = try! decoder.container(keyedBy: AnyCodingKey.self)
con = try! con.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue:"outer1"))
con = try! con.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue:"outer2"))
con = try! con.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue:"outer3"))
let name = try! con.decode(String.self, forKey: AnyCodingKey(stringValue:"name"))
self.name = name
}
}
When I want to dive into the JSON and grab the name information, it's trivial:
let person = try! JSONDecoder().decode(Person.self, from: json)
The result is a Person object with name value "matt". Note that I didn't have to add any of the ignore keys and I didn't need to make a nest of structs.

Sure you can achieve this but with both JSonSerialization & Decodable , you have to serialize the json until reach the desired content then decode it ,but instead I recommend to create structs for root keys only then decode , think of it as it's a path from top to bottom don't decode a key that isn't in the path of your desired content

Related

Unable to parse Json properly with SwiftyJson

I have the folllowing Json response:
` [Body]:
{"id":"cmpl-6Z45N6vfd2312vdfqyYPb8aDRVe9Nft7vfnpoEsL","object":"text_completion","created":16738vfv15205,"model":"text-davinci-003","choices":[{"text":"\n1. Full Stack Developer\n2. Project Engineering Intern\n3. AI Programmer\n4. Systems Trade Studies Engineer\n5. BLE Technology Developer\n6. macOS Menu Bar App Developer\n7. Mobile App Developer\n8. Research Engineer\n9. Writing and Design Lab Researcher","index":0,"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":726,"completion_tokens":60,"total_tokens":786}}`
And I'm using SwiftyJson and Alamofire to parse the Json using the following code:
` AF.request(...).responseJSON { response in
print("response: \(response)")
switch response.result {
case .success(_):
if let JSON = response.value as! [[String : Any]]?{
print("JSON: \(JSON)")
let dic = JSON[0] as [String:AnyObject]?
print("TitularEmail : ",dic?["choices"])
}
break
case .failure(_):
print("There is an error")
break
}
}`
But it keeps breaking and I keep getting the error 'Thread 1: signal SIGABRT' at 'if let JSON = response.value as! [[String : Any]]?{'
Does anyone know what could've gone wrong? I've read through quite some threads but I can't seem to figure this out. Any info shall be appreciated.
Using the built-in Decodable would be a much better alternative. At the risk of slightly wandering from the original SwiftyJSON question, I highly recommend using Decodable to parse JSON (see also Encodable for writing and Codable for the combination of both). Here's an example of a very basic use of Decodable. Supposed you want to parse the following:
{
"name": "New York City",
"mayor": "Eric Adams",
"population": 8468000
}
You can parse it super easily with Decodable:
struct City: Decodable {
let name:String
let mayor:String
let population:Int
}
You can create an instance of City in this example by taking the JSON– assume it is stored in a variable called jsonData– and simply doing the following:
let NYC = try JSONDecoder().decode(City.self, from: jsonData)
You should highly consider this route because it can get a lot done with very little work.
You can also use JSONSerialization if you don't want to use the aforementioned ways of parsing it. Here is a simple tutorial on how to do that.
In your case, you can implement Decodable to solve your problem like this:
var json = try! Data(contentsOf: URL(fileURLWithPath: "PATH TO FILE WITH EXAMPLE JSON TEXT"))
struct FirstStruct:Decodable {
let id:String
let object:String
let created:String //I'm calling this a string because, despite the lack of quotes, it has letters in it and cannot be an Int. You could also do something like Int? so it can fail safely if there are letters. I am adding quotes to the example text make it work.
let model:String
let choices:[SecondStruct]
let usage:ThirdStruct
}
struct SecondStruct:Decodable {
let text:String
let index:Int
let logprobs:String? //idk what type this is supposed to be from the text given
let finish_reason:String
}
struct ThirdStruct:Decodable {
let prompt_tokens:Int
let completion_tokens:Int
let total_tokens:Int
}
var test = try! JSONDecoder().decode(FirstStruct.self, from: json)
print(test.id)
print(test.usage.total_tokens) //these two examples show it clearly works here
Obviously, have a bit more care with error handling, try! was just easier for the example.

Swift Decodable - Is it possible to read part of a tree as a string rather than a data structure?

Here's my problem. Let's say I have a JSON structure that I'm reading using Swift's Codable API. What I want to do is not decode part of the JSON but read it as a string even though it's valid JSON.
In a playground I'm messing about with this code:
import Foundation
let json = #"""
{
"abc": 123,
"def": {
"xyz": "hello world!"
}
}
"""#
struct X: Decodable {
let abc: Int
let def: String
enum CodingKeys: String, CodingKey {
case abc
case def
}
init(decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
abc = try container.decode(Int.self, forKey: .abc)
var defContainer = try container.nestedUnkeyedContainer(forKey: .def)
def = try defContainer.decode(String.self)
// def = try container.decode(String.self, forKey: .def)
}
}
let x = try JSONDecoder().decode(X.self, from: json.data(using: .utf8)!)
Essentially I'm trying to read the def structure as a string instead of a dictionary.
Any clues?
If the resulting string doesn't need to be identical to the corresponding text in the JSON file (i.e. preserve whatever white space is there, etc.), just decode the entire JSON, and then encode the part that you want as a string, and construct a string from the resulting data.
If you do want to preserve exactly the text in the original JSON, including white space, then you'll do better to get that string some other way. Foundation's Scanner class makes it pretty easy to look for some starting token (e.g. "def:") and then read as much data as you want. So consider decoding the JSON in one step, and then separately using a Scanner to dig through the same input data to get the string you need.
Definitely not using JSONDecoder. By the time init(from:) is called, the underlying data has already been thrown away. However you do it, you'll need to parse the JSON yourself. This isn't as hard as it sounds. For example, to extract this string, you could use JSONScanner, which is a few hundred lines of code that you can adjust as you like. With that, you can do things like:
let scanner = JSONScanner()
let string = try scanner.extractData(from: Data(json.utf8), forPath: ["def"])
print(String(data: string, encoding: .utf8)!)
And that will print out:
{
"xyz": "hello world!"
}
(Note that RNAJSON is a sandbox framework of mine. It's not a production-ready framework, but it does a lot of interesting JSON things.)
Integrating this into a system that decodes this in a "Decoder-like" way is definitely buildable along these lines, but there's no simple answer down that path. Caleb's suggestion of re-encoding the data into a JSON string is definitely the easiest way.
Using RNAJSON again, there's a type called JSONValue that you can use like this:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
abc = try container.decode(Int.self, forKey: .abc)
let defJSON = try container.decode(JSONValue.self, forKey: .def)
def = String(data: try JSONEncoder().encode(defJSON), encoding: .utf8)!
}
This will make def be a JSON string, but it doesn't promise that key order is maintained or that whitespace is preserved.
Thanks everyone. It's an interesting problem. Not so much in the rather simple example I gave, but in the actual problem I'm thinking about I want to include mustache templating inside the text I'm decoding (which by the way could be JSON or YAML). The top level parts of the data are fixed in that they have pre-defined keys so reading them is fine. but there is a point in the data where the developer generating it can pretty much include any data structure they like, and that data structure can include mustache keys. ie. abc: {{some-value}} which of course, makes no sense to a decoder.
There's lots of ways this could go wrong but I'm thinking that really I would need to run mustache across the entire data structure before decoding it, where as currently I'm decoding then running mustache and to do that I'm already setting the unknown part as a string and reading it as such. I'm contemplating the idea that it would be much nicer if it was just JSON/YAML rather than a string containing JSON/YAML. But it might not be possible :-)

Swift JsonEncoder element order

I'm currently working on a project were the provided API sadly is a hot mess. I'm encoding a Codable with several nested Types following the first solution in this medium article about Indeterminate Types with Codable in Swift.
So, I'm using an enum to allow different types in my Codable Messages roughly as follows:
struct CommandBase: Encodable {
let command: CommandType
enum CodingKeys: String, CodingKey {
let command = "c"
}
}
enum CommandType: Encodable {
case someCommand(someContainer)
case someOtherCommand(someOtherContainer)
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .someCommand(let someContainer):
try container.encode("something", forkey: .name)
try container.encode(someContainer, forKey: .container)
try container.encode("etc", forKey: .etc)
case .someOtherCommand(let someOtherContainer):
// about the same as above case
}
}
enum CodingKeys: String, CodingKey {
case name
case container
case etc
}
}
The problem is, as I mentioned the API I have to work with is a mess and NEEDS the json in the correct order. But for some reason the Standard JsonEncoder always puts the .container first.
This is a refactor the earlier version used several generic types and resulted in hardly readable code but the order was met.
The JsonEncoder has an outputFormatting attribute which uses a OutputFormatting type like .sortedKeys to sort the keys alphabetical. But I don't understand if and how its possible to utilize this attribute to sort a nested codable.
has anyone an idea how I could sort the encoded message like I need?

escaped json string on Swift

In an iPhone app using SWIFT, I'm having to deal with a third-party API that sends a escaped string instead of a JSON object as response.
The response looks like this:
"[
{
\"ID\":3880,
\"Name\":\"Exploration And Production Inc.\",
\"ContractNumber\":\"123-123\",
\"Location\":\"Booker #1\",
\"Volume\":1225.75,
\"OtherFees\":10.0
}
]"
Up until now I have been dealing with this by manipulating the string to remove the unwanted characters until I get a JSON-like string and then parsing that as usual.
Angular has a handy function to deal with this:
angular.fromJson(response.data);
Java has its own way to deal with it. Is an equivalent function in Swift?
If you are parsing it to a dictionary then the simplest solution will be to convert String to Data and use JSONSerialization:
if let data = string.data(using: .utf8) {
do {
let responseArray = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]]
print(responseArray)
}
catch {
print(error)
}
}
But ofcourse it would be better to process it as a Codable model in which case its again just as simple as:
try JSONDecoder().decode([Item].self, from: data)
Provided that you have a valid Decodable model like so:
struct Item: Decodable {
let id: Int
let name: String
//add the other keys you want to use (note its type sensitive)
enum CodingKeys: String, CodingKey {
case id = "ID"
case name = "Name"
}
}
Lastly, avoid stringified jsons because its an easy source of errors.
Malformed strings or small/large deviations in the structure can go easily unnoticed.
Let the backend team know that they should follow a protocol in their API that its consumers can rely on.
By setting the json format, its essentially like a contract that showcases its content and purpose with clarity.
Sending a stringified json is simply lazy and reflects poorly on its designer, imho.

How to parse JSON into NSManagedObject with Codable? [duplicate]

Codable seems a very exciting feature. But I wonder how we can use it in Core Data? In particular, is it possible to directly encode/decode a JSON from/to a NSManagedObject?
I tried a very simple example:
and defined Foo myself:
import CoreData
#objc(Foo)
public class Foo: NSManagedObject, Codable {}
But when using it like this:
let json = """
{
"name": "foo",
"bars": [{
"name": "bar1",
}], [{
"name": "bar2"
}]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let foo = try! decoder.decode(Foo.self, from: json)
print(foo)
The compiler failed with this errror:
super.init isn't called on all paths before returning from initializer
and the target file was the file that defined Foo
I guess I probably did it wrong, since I didn't even pass a NSManagedObjectContext, but I have no idea where to stick it.
Does Core Data support Codable?
You can use the Codable interface with CoreData objects to encode and decode data, however it's not as automatic as when used with plain old swift objects. Here's how you can implement JSON Decoding directly with Core Data objects:
First, you make your object implement Codable. This interface must be defined on the object, and not in an extension. You can also define your Coding Keys in this class.
class MyManagedObject: NSManagedObject, Codable {
#NSManaged var property: String?
enum CodingKeys: String, CodingKey {
case property = "json_key"
}
}
Next, you can define the init method. This must also be defined in the class method because the init method is required by the Decodable protocol.
required convenience init(from decoder: Decoder) throws {
}
However, the proper initializer for use with managed objects is:
NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)
So, the secret here is to use the userInfo dictionary to pass in the proper context object into the initializer. To do this, you'll need to extend the CodingUserInfoKey struct with a new key:
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")
}
Now, you can just as the decoder for the context:
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
guard let entity = NSEntityDescription.entity(forEntityName: "MyManagedObject", in: context) else { fatalError() }
self.init(entity: entity, in: context)
let container = decoder.container(keyedBy: CodingKeys.self)
self.property = container.decodeIfPresent(String.self, forKey: .property)
}
Now, when you set up the decoding for Managed Objects, you'll need to pass along the proper context object:
let data = //raw json data in Data object
let context = persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
decoder.userInfo[.context] = context
_ = try decoder.decode(MyManagedObject.self, from: data) //we'll get the value from another context using a fetch request later...
try context.save() //make sure to save your data once decoding is complete
To encode data, you'll need to do something similar using the encode protocol function.
CoreData is its own persistence framework and, per its thorough documentation, you must use its designated initializers and follow a rather specific path to creating and storing objects with it.
You can still use Codable with it in limited ways just as you can use NSCoding, however.
One way is to decode an object (or a struct) with either of these protocols and transfer its properties into a new NSManagedObject instance you've created per Core Data's docs.
Another way (which is very common) is to use one of the protocols only for a non-standard object you want to store in a managed object's properties. By "non-standard", I mean anything thst doesn't conform to Core Data's standard attribute types as specified in your model. For example, NSColor can't be stored directly as a Managed Object property since it's not one of the basic attribute types CD supports. Instead, you can use NSKeyedArchiver to serialize the color into an NSData instance and store it as a Data property in the Managed Object. Reverse this process with NSKeyedUnarchiver. That's simplistic and there is a much better way to do this with Core Data (see Transient Attributes) but it illustrates my point.
You could also conceivably adopt Encodable (one of the two protocols that compose Codable - can you guess the name of the other?) to convert a Managed Object instance directly to JSON for sharing but you'd have to specify coding keys and your own custom encode implementation since it won't be auto-synthesized by the compiler with custom coding keys. In this case you'd want to specify only the keys (properties) you want to be included.
Hope this helps.
Swift 4.2:
Following casademora's solution,
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError() }
should be
guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }.
This prevents errors that Xcode falsely recognizes as array slice problems.
Edit: Use implicitly unwrapped optionals to remove the need to force unwrap .context every time it is being used.
As an alternative for those who would like to make use of XCode's modern approach to NSManagedObject file generation, I have created a DecoderWrapper class to expose a Decoder object which I then use within my object which conforms to a JSONDecoding protocol:
class DecoderWrapper: Decodable {
let decoder:Decoder
required init(from decoder:Decoder) throws {
self.decoder = decoder
}
}
protocol JSONDecoding {
func decodeWith(_ decoder: Decoder) throws
}
extension JSONDecoding where Self:NSManagedObject {
func decode(json:[String:Any]) throws {
let data = try JSONSerialization.data(withJSONObject: json, options: [])
let wrapper = try JSONDecoder().decode(DecoderWrapper.self, from: data)
try decodeWith(wrapper.decoder)
}
}
extension MyCoreDataClass: JSONDecoding {
enum CodingKeys: String, CodingKey {
case name // For example
}
func decodeWith(_ decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
}
}
This is probably only useful for models without any non-optional attributes, but it solves my problem of wanting to use Decodable but also manage relationships and persistence with Core Data without having to manually create all my classes / properties.
Edit: Example of it in use
If I have a json object:
let myjson = [ "name" : "Something" ]
I create the object in Core Data (force cast here for brevity):
let myObject = NSEntityDescription.insertNewObject(forEntityName: "MyCoreDataClass", into: myContext) as! MyCoreDataClass
And I use the extension to have the object decode the json:
do {
try myObject.decode(json: myjson)
}
catch {
// handle any error
}
Now myObject.name is "Something"