Custom Decodable JSON - Get first array element - ios

I have JSON data that looks like this:
{
// Other attributes here....
"id": "a4s5d6f8ddw",
"images": {
"selection": [
{
"url": "https://www.myimage.com"
},
// ... more images here
]
}
}
I want to extract the id and the first url from the nested JSON and store it in my struct. I've tried something like this, but I can't quite get it:
struct MyImage {
let id: String
let url: URL
enum CodingKeys: CodingKey {
case id
case images
}
enum Selection: CodingKey {
case selection
}
enum ImageURL: CodingKey {
case url
}
}
extension MyImage: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
let selection = try values.nestedContainer(keyedBy: Selection.self, forKey: .selection)
// Then what?? Need to extract just the first one and then decode to a URL
}
How can I finish this off?

You're on the right track, but some minor errors:
struct MyImage {
let id: String
let url: URL
enum CodingKeys: CodingKey {
case id
case images
}
// Keys inside of Images
enum ImagesKeys: CodingKey {
case selection
}
// Struct inside of Selection
private struct Selection: Decodable {
let url: URL
}
}
With that, the decoder is close to what you were thinking:
extension MyImage: Decodable {
init(from decoder: Decoder) throws {
// Decode the outer stuff
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
// Pull off the images object
let images = try values.nestedContainer(keyedBy: ImagesKeys.self, forKey: .images)
// Extract the selections array
var selections = try images.nestedUnkeyedContainer(forKey: .selection)
// Take the first element
let firstSelection = try selections.decode(Selection.self)
// Done.
self.url = firstSelection.url
}
}

Related

Asymmetric Encoding/Decoding with Codable and Firestore?

I have the following struct:
struct Recipe: Codable {
#DocumentID var id: String?
var vegetarian: Bool?
private enum CodingKeys: String, CodingKey {
case id
case vegetarian
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
vegetarian = try container.decode(Bool.self, forKey: .vegetarian)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(vegetarian, forKey: .vegetarian)
}
}
I am trying to:
Decode only vegetarian
Encode both vegetarian and id
This is my data model:
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.recipe = try document.data(as: Recipe.self)
let recipeFromFirestore = Recipe(
id: self.recipe!.id,
vegetarian: self.recipe!.vegetarian)
self.recipes.append(recipeFromFirestore)
}
catch {
print("Error: \(error)")
}
}
}
}
I'm getting the following error in my getDocument: Missing argument for parameter 'from' in call.
This error doesn't happen if I comment out my init(from decoder: Decoder) and func encode(to encoder: Encoder) in my struct. Should I be doing something different for this asymmetric encoding/decoding?
I don't get where you are seeing that error from - a line reference would be useful - but it's not directly related to your en/decoding. (As an aside, that method really is not a data model)
As you want to decode both properties with JSON keys that match their property names, there is no need to specify CodingKeys or write a custom decoder; you can rely on the synthesised decoder and let Codable do the work for you.
For the decoding you will need a custom solution else Decodable will decode both fields. This will require both a CodingKey enum (note the singular, i.e. the protocol, not the defaut enun name) and a custom encoder to use that.
You end up with a far simpler implementation of your struct. I've also added a simple initialiser as you lose the synthesised memberwise initialiser as soon as you define the init(from:). This was just so I could test it.
struct Recipe: Codable {
var id: String?
var vegetarian: Bool?
init(id: String, vegetarian: Bool){
self.id = id
self.vegetarian = vegetarian
}
init(from decoder: Decoder) throws {
enum DecodingKeys: CodingKey {
case vegetarian
}
let container = try decoder.container(keyedBy: DecodingKeys.self)
vegetarian = try container.decode(Bool.self, forKey: .vegetarian)
}
}
If you test this you will find that it will just decode the vegetarian property but encode both. Simple testing shows:
let recipe = Recipe(id: "1", vegetarian: true)
let data = try! JSONEncoder().encode(recipe)
print(String(data: data, encoding: .utf8)!) //{"id":"1","vegetarian":true}
let decodedRecipe = try! JSONDecoder().decode(Recipe.self, from: data)
print(decodedRecipe) //Recipe(id: nil, vegetarian: Optional(true))

Swift - Initialise model object with init(from decoder:)

Below is my model struct
struct MovieResponse: Codable {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
self.response = try values.decodeIfPresent(String.self, forKey: .response)!
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
}
}
extension MovieResponse {
struct Movie: Codable, Identifiable {
var id = UUID()
var title: String
var year: Int8
var imdbID: String
var type: String
var poster: URL
enum EncodingKeys: String, CodingKey {
case title = "Title"
case year = "Year"
case imdmID
case type = "Type"
case poster = "Poster"
}
}
}
Now in a ViewModel, I am creating an instance of this model using the below code
#Published var movieObj = MovieResponse()
But there is a compile error saying, call init(from decoder) method. What is the proper way to create a model instance in this case?
As the Swift Language Guide reads:
Swift provides a default initializer for any structure or class that provides default values for all of its properties and doesn’t provide at least one initializer itself.
The "and doesn’t provide at least one initializer itself" part is crucial here. Since you are declaring an additional initializer you should either declare your own initialized like so:
init(
totalResults: Int,
response: String,
error: String,
movies: [Movie]
) {
self.totalResults = totalResults
self.response = response
self.error = error
self.movies = movies
}
or move Codable conformance to an extension so Swift can provide you with a default initialiser. This would be a preferred way to do it (my personal opinion, I like to move additional protocol conformances to extensions).
struct MovieResponse {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
}
extension MovieResponse: Codable {
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
self.response = try values.decodeIfPresent(String.self, forKey: .response)!
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
}
}
You need to add another initializer if you do not want to use a decoder. Swift give you one for free if and only if you do not write your own initializer. Now that you have one you loose the free one.
add another:
init() {
//Your initializer code here
}
If you are trying to use the decoder init you need to use a decoder to invoke it. For instance if it's Json
#Published var movieObj = try? JSONDecoder().decode(MovieResponse.self, from: <#T##Data#>)

How to decode json that has dynamic key values in swift?

I am having a little trouble constructing my Codable model correctly in swift. I have a json that can have dynamic id key values and one key value that I know is always the same. How do I deal with the dynamic id values? From my research, it looks like I need a custom decoder init, but I don't believe I am doing it 100% correctly. Thank you.
Example json:
{
"1f73433230": "Clark Kent",
"f1c3432fd6": "Batman",
"3d34457d69": "Wonder Woman",
"OTHER_ID": "Other"
}
Code
struct SuperHeroIds: Codable, Equatable {
let id: String
let otherId: String
enum CodingKeys: String, CodingKey {
case otherID = "OTHER_ID"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherId = try container.decode(String.self, forKey: .otherID)
id = String()
???
}
}
I would prefer
let res = try JSONDecoder().decode([String:String].self, from:data)
You can use custom CodingKey
struct AnyKey: CodingKey {
enum Errors: Error {
case invalid
}
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = Int(stringValue)
}
init?(intValue: Int) {
self.intValue = intValue
stringValue = "\(intValue)"
}
static func key(named name: String) throws -> Self {
guard let key = Self(stringValue: name) else {
throw Errors.invalid
}
return key
}
}
and decode in init(from decoder: Decoder) throws like so:
struct SuperHeroIds: Decodable, Equatable {
let id: String
let otherId: String
let idToHeroMap: [String:String]
enum CodingKeys: String, CodingKey, CaseIterable {
case otherID = "OTHER_ID"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherId = try container.decode(String.self, forKey: .otherID)
id = String()
let heroContainer = try decoder.container(keyedBy: AnyKey.self)
// all keys that not decoded with CodingKeys
let decodedKeys = CodingKeys.allCases.map({ $0.rawValue })
let filteredKeys = heroContainer.allKeys.filter({ !decodedKeys.contains($0.stringValue) })
// fill idToHeroMap
var result = [String:String](minimumCapacity: filteredKeys.count)
for key in filteredKeys {
result[key.stringValue] = try heroContainer.decode(String.self, forKey: key)
}
self.idToHeroMap = result
}
}

Is it possible to have an array of structs which conform to the same protocol also support Codable?

I have setup the following protocol, and have 2 structs which then conform to this protocol:
protocol ExampleProtocol: Decodable {
var name: String { get set }
var length: Int { get set }
}
struct ExampleModel1: ExampleProtocol {
var name: String
var length: Int
var otherData: Array<String>
}
struct ExampleModel2: ExampleProtocol {
var name: String
var length: Int
var dateString: String
}
I want to deserialise some JSON data I receive from the server, and I know it will be returning a mix of both ExampleModel1 and ExampleModel2 in an array:
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
}
Is there anyway to use the Codable approach and support both models easily? Or will I need to manually deserialise the data for each model?
EDIT 1:
Conforming to Decodable on the structs, still gives the same results:
struct ExampleModel1: ExampleProtocol, Decodable {
enum CodingKeys: String, CodingKey {
case name, length, otherData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.length = try container.decode(Int.self, forKey: .length)
self.otherData = try container.decode(Array<String>.self, forKey: .otherData)
}
var name: String
var length: Int
var otherData: Array<String>
}
struct ExampleModel2: ExampleProtocol, Decodable {
enum CodingKeys: String, CodingKey {
case name, length, dateString
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.length = try container.decode(Int.self, forKey: .length)
self.dateString = try container.decode(String.self, forKey: .dateString)
}
var name: String
var length: Int
var dateString: String
}
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
}
If you have a limited amount of ExampleProtocols and you need to have a different type of ExampleProtocols in the same array, then you can create a holder for ExampleProtocol and use it for decoding/encoding.
ExampleHolder could hold all possible Decodable ExampleProtocol types in one array. So decoder init don't need to have so many if-else scopes and easier to add more in the future.
Would recommend keeping ExampleHolder as a private struct. So it's not possible to access it outside of file or maybe even not outside of ExampleNetworkResponse.
enum ExampleNetworkResponseError: Error {
case unsupportedExampleModelOnDecoding
}
private struct ExampleHolder: Decodable {
let exampleModel: ExampleProtocol
private let possibleModelTypes: [ExampleProtocol.Type] = [
ExampleModel1.self,
ExampleModel2.self
]
init(from decoder: Decoder) throws {
for type in possibleModelTypes {
if let model = try? type.init(from: decoder) {
exampleModel = model
return
}
}
throw ExampleNetworkResponseError.unsupportedExampleModelOnDecoding
}
}
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
enum CodingKeys: String, CodingKey {
case someString, modelArray
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
someString = try container.decode(String.self, forKey: .someString)
let exampleHolderArray = try container.decode([ExampleHolder].self, forKey: .modelArray)
modelArray = exampleHolderArray.map({ $0.exampleModel })
}
}
–––––––––––––––––––––––––––––––––
If in one response can have only one type of ExampleProtocol in the array then:
struct ExampleNetworkResponse2<ModelArrayElement: ExampleProtocol>: Decodable {
var someString: String
var modelArray: Array<ModelArrayElement>
}
usage:
let decoder = JSONDecoder()
let response = try decoder.decode(
ExampleNetworkResponse2<ExampleModel1>.self,
from: dataToDecode
)

Unable to access codable struct values in Swift

I am fetching user's data as json from a service and decoding into Codable User struct. I can access that property where I've fetched the response but I want to access that User struct property somewhere else, let's say in another class, function, etc.
I'm new to this and I'm thinking the ideal approach is to "at the time of fetching, store that data into Core Data or User Defaults and then update my views accordingly.
Please suggest what's the best and appropriate approach or wether there is any way to access codable struct values directly.
Here is my codable struct -
struct User: Codable {
let name : String?
enum CodingKeys: String, CodingKey {
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
The wrong way I'm accessing struct in some function is -
UserInfo.CodingKeys.name.rawValue
//Output is the key string - 'Name'
I think static can help you
struct User: Codable {
let name : String?
private enum CodingKeys: String, CodingKey {
case name = "Name"
}
}
assume fetching data here
class FetchingClass{
static var user: User?
func fetchData(){
//After Url request success
do {
//assign directly to static varibale user
FetchingClass.user = try JSONDecoder().decode(User.self, from: data)
} catch _ {
}
}
}
use like this wherever you want without using coreData or UserDefaults
class AnotherClass{
func anotherClassFunc(){
//use if let or guard let
if let user = FetchingClass.user{
print(user.name)
}
//or
if let FetchingClass.user != nil {
print(FetchingClass.name)
}
}
}
You can try using a singleton reference
struct User: Codable {
let name : String?
enum CodingKeys: String, CodingKey {
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
User.shared = self
}
}
Create another class which hold the ref. of User
final class AppGlobalManager {
static let sharedInstance = AppGlobalManager()
var currentUser: User? // Initialise it when user logged in
private init()
}
Then you can access any of the user data as
AppGlobalManager.sharedInstance.currentUser.name

Resources