Swift - Connecting Contentful Data via multiple Classes - ios

I'm trying to get my data from Contentful in my Swift app but stuck on getting a dictionary.
UPDATE: The type is Link & linkType is Entry. So I need to decode a link to an entry.
So within my contentful data for Item A I have:
"itemB": {
"sys": {
"type": "Link",
"linkType": "Entry",
"id": "id_is_here"
}
},
But in my code my itemB is nil.
Here's what I have tried:
// Item A
class ItemA: EntryDecodable, FieldKeysQueryable {
static let contentTypeId: String = "ItemA"
// FlatResource Memberes.
let id: String
var updatedAt: Date?
var createdAt: Date?
var localeCode: String?
var itemB: ItemB?
public required init(from decoder: Decoder) throws {
let sys = try decoder.sys()
id = sys.id
localeCode = sys.locale
updatedAt = sys.updatedAt
createdAt = sys.createdAt
let fields = try decoder.contentfulFieldsContainer(keyedBy: FieldKeys.self)
try fields.resolveLink(forKey: .itemB, decoder: decoder) { [weak self] item in
self?.itemB = item as? ItemB
}
}
enum FieldKeys: String, CodingKey {
case itemB
}
public init(sys: Sys, itemB: ItemB?) {
id = sys.id
localeCode = sys.locale
updatedAt = sys.updatedAt
createdAt = sys.createdAt
self.itemB = itemB
}
}
// Item B
class ItemB: EntryDecodable, FieldKeysQueryable {
static let contentTypeId: String = "ItemB"
// FlatResource Memberes.
let id: String
var updatedAt: Date?
var createdAt: Date?
var localeCode: String?
let name: String?
public required init(from decoder: Decoder) throws {
let sys = try decoder.sys()
id = sys.id
localeCode = sys.locale
updatedAt = sys.updatedAt
createdAt = sys.createdAt
let fields = try decoder.contentfulFieldsContainer(keyedBy: FieldKeys.self)
self.name = try fields.decode(String.self, forKey: .name)
}
enum FieldKeys: String, CodingKey {
case name
}
public init(sys: Sys, name: String) {
id = sys.id
localeCode = sys.locale
updatedAt = sys.updatedAt
createdAt = sys.createdAt
self.name = name
}
}

Related

Issue with unwrapping optionals in my model and DTO

This might be something really easy but I don't understand how to do it:
so I have this DTO struct I use to get API data into it and map it to Model struct
my DTO:
struct PetDTO: Codable {
var id: Int
var category: CategoryDTO?
var name: String?
var photoUrls: [String]?
var tags: [TagDTO]?
var status: StatusDTO?
}
public class CategoryDTO: NSObject, Codable {
var id: Int
var name: String?
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
}
}
public class TagDTO: NSObject, Codable {
var id: Int
var name: String?
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
}
}
enum StatusDTO: String, Codable {
case available
case sold
case pending
}
And my model:
struct PetDataModel {
var id: Int
var category: Category
var name: String?
var photoUrls: [String]?
var tags: [Tags]?
var status: Status?
init(petDto: PetDTO) {
self.id = petDto.id
self.category = Category(categoryDto: petDto.category)
self.name = petDto.name
self.photoUrls = petDto.photoUrls
for tag in petDto.tags ?? [] {
self.tags = [Tags(tagDTO: tag)] // petDto?.map { Tags(tagDTO: $0) }
}
self.status = Status(rawValue: petDto.status?.rawValue)
}
}
struct Category {
var id: Int
var name: String?
init(categoryDto: CategoryDTO) {
self.id = categoryDto.id
self.name = categoryDto.name
}
}
struct Tags {
var id: Int
var name: String?
init(tagDTO: TagDTO) {
self.id = tagDTO.id
self.name = tagDTO.name
}
}
enum Status: String, Codable {
case available
case sold
case pending
}
As you can see, the mapping happens in Init of PetDataModel. I have errors on this lines
Please tell me how to fix this without making CategoryDto from PetDTO non optional, I need it to stay optional.
You can make category form your PetDataModel optional too.
struct PetDataModel {
var id: Int
var category: Category?
var name: String?
var photoUrls: [String]?
var tags: [Tags]?
var status: Status?
init(petDto: PetDTO) {
self.id = petDto.id
self.category = Category(categoryDto: petDto.category)
self.name = petDto.name
self.photoUrls = petDto.photoUrls
for tag in petDto.tags ?? [] {
self.tags = [Tags(tagDTO: tag)] // petDto?.map { Tags(tagDTO: $0) }
}
self.status = Status(rawValue: petDto.status?.rawValue)
}
}
and make your initializer optional:
struct Category {
var id: Int
var name: String?
init?(categoryDto: CategoryDTO?) {
guard let categoryDto = categoryDto else{
return nil
}
self.id = categoryDto.id
self.name = categoryDto.name
}
}
if you donĀ“t want an optional initializer you can check and assign like this:
self.category = petDto.category != nil ? Category(categoryDto: petDto.category!) : nil

Is there any way of initializing variables and let constants in an API response model without defining their type? [duplicate]

I have an API that will sometimes return a specific key value (in this case id) in the JSON as an Int and other times it will return that same key value as a String. How do I use codable to parse that JSON?
struct GeneralProduct: Codable {
var price: Double!
var id: String?
var name: String!
private enum CodingKeys: String, CodingKey {
case price = "p"
case id = "i"
case name = "n"
}
init(price: Double? = nil, id: String? = nil, name: String? = nil) {
self.price = price
self.id = id
self.name = name
}
}
I keep getting this error message: Expected to decode String but found a number instead. The reason that it returns a number is because the id field is empty and when the id field is empty it defaults to returning 0 as an ID which codable identifies as a number. I can basically ignore the ID key but codable does not give me the option to ignore it to my knowledge. What would be the best way to handle this?
Here is the JSON. It is super simple
Working
{
"p":2.12,
"i":"3k3mkfnk3",
"n":"Blue Shirt"
}
Error - because there is no id in the system, it returns 0 as a default which codable obviously sees as a number opposed to string.
{
"p":2.19,
"i":0,
"n":"Black Shirt"
}
struct GeneralProduct: Codable {
var price: Double?
var id: String?
var name: String?
private enum CodingKeys: String, CodingKey {
case price = "p", id = "i", name = "n"
}
init(price: Double? = nil, id: String? = nil, name: String? = nil) {
self.price = price
self.id = id
self.name = name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
price = try container.decode(Double.self, forKey: .price)
name = try container.decode(String.self, forKey: .name)
do {
id = try String(container.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}
}
}
let json1 = """
{
"p":2.12,
"i":"3k3mkfnk3",
"n":"Blue Shirt"
}
"""
let json2 = """
{
"p":2.12,
"i":0,
"n":"Blue Shirt"
}
"""
do {
let product = try JSONDecoder().decode(GeneralProduct.self, from: Data(json2.utf8))
print(product.price ?? "nil")
print(product.id ?? "nil")
print(product.name ?? "nil")
} catch {
print(error)
}
edit/update:
You can also simply assign nil to your id when your api returns 0:
do {
let value = try container.decode(Int.self, forKey: .id)
id = value == 0 ? nil : String(value)
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}
This is a possible solution with MetadataType, the nice thing is that can be a general solution not for GeneralProduct only, but for all the struct having the same ambiguity:
struct GeneralProduct: Codable {
var price:Double?
var id:MetadataType?
var name:String?
private enum CodingKeys: String, CodingKey {
case price = "p"
case id = "i"
case name = "n"
}
init(price:Double? = nil, id: MetadataType? = nil, name: String? = nil) {
self.price = price
self.id = id
self.name = name
}
}
enum MetadataType: Codable {
case int(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .int(container.decode(Int.self))
} catch DecodingError.typeMismatch {
do {
self = try .string(container.decode(String.self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(MetadataType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let int):
try container.encode(int)
case .string(let string):
try container.encode(string)
}
}
}
this is the test:
let decoder = JSONDecoder()
var json = "{\"p\":2.19,\"i\":0,\"n\":\"Black Shirt\"}"
var product = try! decoder.decode(GeneralProduct.self, from: json.data(using: .utf8)!)
if let id = product.id {
print(id) // 0
}
json = "{\"p\":2.19,\"i\":\"hello world\",\"n\":\"Black Shirt\"}"
product = try! decoder.decode(GeneralProduct.self, from: json.data(using: .utf8)!)
if let id = product.id {
print(id) // hello world
}
Seamlessly decoding from either Int or String into the same property requires writing some code.
However, thanks to a (somewhat) new addition to the language,(property wrappers), you can make it quite easy to reuse this logic wherever you need it:
// note this is only `Decodable`
struct GeneralProduct: Decodable {
var price: Double
#Flexible var id: Int // note this is an Int
var name: String
}
The property wrapper and its supporting code can be implemented like this:
#propertyWrapper struct Flexible<T: FlexibleDecodable>: Decodable {
var wrappedValue: T
init(from decoder: Decoder) throws {
wrappedValue = try T(container: decoder.singleValueContainer())
}
}
protocol FlexibleDecodable {
init(container: SingleValueDecodingContainer) throws
}
extension Int: FlexibleDecodable {
init(container: SingleValueDecodingContainer) throws {
if let int = try? container.decode(Int.self) {
self = int
} else if let string = try? container.decode(String.self), let int = Int(string) {
self = int
} else {
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Invalid int value"))
}
}
}
Original answer
You can use a wrapper over a string that knows how to decode from any of the basic JSON data types: string, number, boolean:
struct RelaxedString: Codable {
let value: String
init(_ value: String) {
self.value = value
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// attempt to decode from all JSON primitives
if let str = try? container.decode(String.self) {
value = str
} else if let int = try? container.decode(Int.self) {
value = int.description
} else if let double = try? container.decode(Double.self) {
value = double.description
} else if let bool = try? container.decode(Bool.self) {
value = bool.description
} else {
throw DecodingError.typeMismatch(String.self, .init(codingPath: decoder.codingPath, debugDescription: ""))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
You can then use this new type in your struct. One minor disadvantage would be that consumer of the struct will need to make another indirection to access the wrapped string. However that can be avoided by declaring the decoded RelaxedString property as private, and use a computed one for the public interface:
struct GeneralProduct: Codable {
var price: Double!
var _id: RelaxedString?
var name: String!
var id: String? {
get { _id?.value }
set { _id = newValue.map(RelaxedString.init) }
}
private enum CodingKeys: String, CodingKey {
case price = "p"
case _id = "i"
case name = "n"
}
init(price: Double? = nil, id: String? = nil, name: String? = nil) {
self.price = price
self._id = id.map(RelaxedString.init)
self.name = name
}
}
Advantages of the above approach:
no need to write custom init(from decoder: Decoder) code, which can become tedious if the number of properties to be decoded increase
reusability - RelaxedString can be seamlessly used in other structs
the fact that the id can be decoded from a string or an int remains an implementation detail, consumers of GeneralProduct don't know/care that the id can come from a string or an int
the public interface exposes string values, which keeps the consumer code simple as it will not have to deal with multiple types of data
I created this Gist which has a ValueWrapper struct that can handle
the following types
case stringValue(String)
case intValue(Int)
case doubleValue(Double)
case boolValue(Bool)
https://gist.github.com/amrangry/89097b86514b3477cae79dd28bba3f23
Based on #Cristik 's answer, I come with another solution using #propertyWrapper.
#propertyWrapper
struct StringForcible: Codable {
var wrappedValue: String?
enum CodingKeys: CodingKey {}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
wrappedValue = string
} else if let integer = try? container.decode(Int.self) {
wrappedValue = "\(integer)"
} else if let double = try? container.decode(Double.self) {
wrappedValue = "\(double)"
} else if container.decodeNil() {
wrappedValue = nil
}
else {
throw DecodingError.typeMismatch(String.self, .init(codingPath: container.codingPath, debugDescription: "Could not decode incoming value to String. It is not a type of String, Int or Double."))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
init() {
self.wrappedValue = nil
}
}
And usage is
struct SomeDTO: Codable {
#StringForcible var id: String?
}
Also works like -I think-
struct AnotherDTO: Codable {
var some: SomeDTO?
}

Swift 5 How do we parse a Dictionary nested inside a Dictionary, nested inside another Dictionary?

This is the json at hand. I am able to parse all the data except the "clientDetails" dictionary. Here is my code. Im just not sure how to go about mapping the "clientDetails" dictionary. Is there a completely different approach i should be taking? or am I on the right track here..
{
"data": {
"userID": "124",
"techID": "1233",
"clientID": "1",
"Name": "Haibert",
"lastName": "Chiem",
"emailAddress": "haibert#simplygood.com",
"additionalCompanies": "",
"clientDetails": {
"clientID": "1",
"FirstName": "Sam",
"LastName": "Ka",
"email": "rem#simplygood.com",
"CustomerName": "SimplyRem",
"BusinessAddress": "9863 N. Glenoaks Blvd, Ste. 207F",
"BusinessCity": "Burbank",
"BusinessState": "",
"BusinessZip": "91502",
"BusinessCountry": "USA",
"BusinessPhone": "(818)457-9507",
"BusinessFax": null,
"MobilePhone": "818-378-9507",
"ContactName": "Samuel Kaz",
"ContactTitle": "Owner",
"ContactPhone": "818-624-9507",
"ContactEmail": "sam#simplygood.com",
"lastUpdated": "2020-10-24 15:09:17"
}
}
}
struct UserInformation: Decodable {
var userID: String = ""
var clientID: String = ""
var Name: String = ""
var lastName: String = ""
var emailAddress: String = ""
var techID: String = ""
var additionalCompanies: String = ""
private enum DataKeys: String, CodingKey {
case userID = "userID"
case clientID = "clientID"
case Name = "Name"
case lastName = "lastName"
case emailAddress = "emailAddress"
case techID = "techID"
case additionalCompanies = "additionalCompanies"
}
private enum DataResponseKeys: String, CodingKey {
case data
}
init(from decoder: Decoder) throws {
if let dataResponseContainer = try? decoder.container(keyedBy: DataResponseKeys.self) {
if let dataContainer = try? dataResponseContainer.nestedContainer(keyedBy: DataKeys.self, forKey: .data) {
self.userID = try dataContainer.decode(String.self, forKey: .userID)
self.clientID = try dataContainer.decode(String.self, forKey: .clientID)
self.Name = try dataContainer.decode(String.self, forKey: .Name)
self.lastName = try dataContainer.decode(String.self, forKey: .lastName)
self.emailAddress = try dataContainer.decode(String.self, forKey: .emailAddress)
self.techID = try dataContainer.decode(String.self, forKey: .techID)
self.additionalCompanies = try dataContainer.decode(String.self, forKey: .additionalCompanies)
}
}
}
}
Try the following data model
let userInformation = try? JSONDecoder().decode(UserInformation.self, from: jsonData)
import Foundation
// MARK: - UserInformation
struct UserInformation: Codable {
let data: DataClass
}
// MARK: - DataClass
struct DataClass: Codable {
let userID, techID, clientID, name: String
let lastName, emailAddress, additionalCompanies: String
let clientDetails: ClientDetails
enum CodingKeys: String, CodingKey {
case userID, techID, clientID
case name = "Name"
case lastName, emailAddress, additionalCompanies, clientDetails
}
}
// MARK: - ClientDetails
struct ClientDetails: Codable {
let clientID, firstName, lastName, email: String
let customerName, businessAddress, businessCity, businessState: String
let businessZip, businessCountry, businessPhone: String
let businessFax: String? // null value
let mobilePhone, contactName, contactTitle, contactPhone: String
let contactEmail, lastUpdated: String
enum CodingKeys: String, CodingKey {
case clientID
case firstName = "FirstName"
case lastName = "LastName"
case email
case customerName = "CustomerName"
case businessAddress = "BusinessAddress"
case businessCity = "BusinessCity"
case businessState = "BusinessState"
case businessZip = "BusinessZip"
case businessCountry = "BusinessCountry"
case businessPhone = "BusinessPhone"
case businessFax = "BusinessFax"
case mobilePhone = "MobilePhone"
case contactName = "ContactName"
case contactTitle = "ContactTitle"
case contactPhone = "ContactPhone"
case contactEmail = "ContactEmail"
case lastUpdated
}
}
For reference please follow documentation from
encoding_and_decoding_custom_types Apple documentation
raywenderlich encoding-and-decoding-in-swift

Attempting to parse dynamic JSON in iOS

I have produced the following sample block of JSON. Any value that ends with a letter is dynamic.
{
"groupName": {
"groupA": {
"fields": {
"fieldA": "valueA",
"fieldB": "valueB"
},
"letters": {
"letterA: "A"
}
},
"groupB": {
"fields": {
"fieldC": "valueC",
"fieldD": "valueD"
},
"letters": {
"letterB: "B"
}
}
}
}
My goal is to use Decodable so that I may read this data into structs that I have defined.
Below is my current work contained in a playground file that I am using to try and resolve this:
import Foundation
let jsonString = "{\"groupName\":{\"groupA\":{\"fields\":{\"fieldA\":\"valueA\",\"fieldB\":\"valueB\"},\"letters\":{\"letterA:\"A\"}},\"groupB\":{\"fields\":{\"fieldC\":\"valueC\",\"fieldD\":\"valueD\"},\"letters\":{\"letterB:\"B\"}}}}"
struct CustomCodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
static let field = CustomCodingKeys.make(key: "field")
static func make(key: String) -> CustomCodingKeys {
return CustomCodingKeys(stringValue: key)!
}
}
// Values
struct Field {
let field: String
let value: String
}
struct Letter: Decodable {
let title: String
let letter: String
}
// Value holders
struct FieldData: Decodable {
var fields: [Field]
init(from decoder: Decoder) throws {
self.fields = [Field]()
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
for key in container.allKeys {
print("processing field: \(key.stringValue)")
let dynamicKey = CustomCodingKeys.make(key: key.stringValue)
let value = try container.decode(String.self, forKey: dynamicKey)
let field = Field(field: key.stringValue,
value: value)
fields.append(field)
}
}
}
struct LetterData: Decodable {
var letters: [Letter]
init(from decoder: Decoder) throws {
self.letters = [Letter]()
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
for key in container.allKeys {
print("processing letter: \(key.stringValue)")
let dynamicKey = CustomCodingKeys.make(key: key.stringValue)
let value = try container.decode(String.self, forKey: dynamicKey)
let letter = Letter(title: key.stringValue,
letter: value)
letters.append(letter)
}
}
}
// Containers
struct Group: Decodable {
var name: String!
var groups: [GroupData]
init(from decoder: Decoder) throws {
self.groups = [GroupData]()
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
for key in container.allKeys {
print("processing section: \(key.stringValue)")
let group = try container.decode(GroupData.self,
forKey: key)
groups.append(group)
}
}
}
struct GroupData: Decodable {
var fieldData: FieldData
var letterData: LetterData
enum CodingKeys: String, CodingKey {
case fieldData = "fields"
case letterData = "letters"
}
}
struct GroupList: Decodable {
struct GroupName: Decodable {
var name: String!
var groups: [Group]
init(from decoder: Decoder) throws {
self.groups = [Group]()
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
for key in container.allKeys {
let name = key.stringValue
self.name = name
print("processing group: \(String(describing: self.name))")
var group = try container.decode(Group.self,
forKey: key)
group.name = name
groups.append(group)
}
}
}
let groupName: GroupName
}
let decoder = JSONDecoder()
if let data = jsonString.data(using: .utf8),
let groupList = try? decoder.decode(GroupList.self,
from: data) {
print("group list created")
}
In my GroupData struct, I can drop the variables and then implement init(from decoder: Decoder) throws, which when configured with the proper lookups (the FieldData & LetterData inits), can identify the correct pairs. However, it does not populate the proper value structs.
You have small mistake in decoding Group. You tend to decode all keys for group inside Group and also you pass further the to decode GroupData which itself has "fields" and "letters". Use single value container inside Group and it should be fine.
Here is how your Group should look,
struct Group: Decodable {
var name: String!
var groups: GroupData
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
groups = try container.decode(GroupData.self)
}
}
Note, your json itself is incorrect, I have formatted it and it should rather be like this,
let jsonString = "{\"groupName\":{\"groupA\":{\"fields\":{\"fieldA\":\"valueA\",\"fieldB\":\"valueB\"},\"letters\":{\"letterA\":\"A\"}},\"groupB\":{\"fields\":{\"fieldC\":\"valueC\",\"fieldD\":\"valueD\"},\"letters\":{\"letterB\":\"B\"}}}}"

Swift - JSON Serialisation of an nested object

I have a object that is created from JSON (serialised). This object has an id property that represents another object stored in the system. How can I get the nested object during the serialisation?
import UIKit
class Person: Codable {
let firstName: String
let lastName: String
let addressId: String
let address: Address // How to create it during serialisation
private enum CodingKeys: String, CodingKey {
case firstName
case lastName
case addressId = "addressId"
}
init(firstName: String, lastName: String, addressId:String) {
self.firstName = firstName
self.lastName = lastName
self.addressId = addressId
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.firstName = try values.decode(String.self, forKey: .firstName)
self.lastName = try values.decode(String.self, forKey: .lastName)
self.addressId = try? values.decode(URL.self, forKey: .addressId)
}
}
struct PersonsList : Codable {
let persons: [Person]
}
class Address {
static func getAddress(addressId: String) -> Address
{
//some code
return address
}
}
Do it with lazy property
EDIT
Option1
lazy var address:Address = { [unowned self] in
return Address.getAddress(addressId: self.addressId)
}()
Option 2
var adreess1:Address {
return Address.getAddress(addressId: self.addressId)
}
JSON
Assuming this is your json
let json = """
[
{
"firstName": "James",
"lastName": "Kirk",
"address": { "id": "efg" }
}
]
"""
Model
You can simplify how you define your model
struct Person: Codable {
let firstName: String
let lastName: String
let address: Address
struct Address: Codable {
let id: String
}
}
As you can see there is no need to write your custom init(:from)
From JSON to Data
To test it not we are going to transform the json into a Data value.
let data = json.data(using: .utf8)!
Decoding
And finally we can decode the data
if let persons = try? JSONDecoder().decode([Person].self, from: data) {
print(persons.first?.address.id)
}
Output
Optional("efg")

Resources