Swift 4 Decodable multiple containers - json

I'm trying to understand how could I parse this multiple container JSON to an object. I've tried this approach (Mark answer), but he explain how to solve it using one-level container. For some reason I can't mimic the behaviour for multiple containers.
{
"graphql": {
"shortcode_media": {
"id": "1657677004214306744",
"shortcode": "BcBQHPchwe4"
}
}
}
class Post: Decodable {
enum CodingKeys: String, CodingKey {
case graphql // The top level "user" key
case shortcode_media
}
enum PostKeys: String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let post = try values.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)
self.id = try post.decode(String.self, forKey: .id)
}
var id: String
}
I'm getting:
Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot get KeyedDecodingContainer<PostKeys> -- no value found for key \"shortcode_media\"", underlyingError: nil))
Any help will be much appreciated, thank you!

As vadian notes, you haven't matched the JSON structure. There is no shortcode_media key at the top level like you've encoded in CodingKeys.
In order to decode this with a custom decoder, you will need to walk through each level and deal with it.
class Post: Decodable {
enum CodingKeys: String, CodingKey {
case graphql
}
enum GraphQLKeys: String, CodingKey {
case shortcode_media
}
enum PostKeys: String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
// unload the top level
let container = try decoder.container(keyedBy: CodingKeys.self)
// Unload the graphql key
let graphql = try container.nestedContainer(keyedBy: GraphQLKeys.self, forKey: .graphql)
// unload the shortcode_media key
let post = try graphql.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)
// Finally, unload the actual object
self.id = try post.decode(String.self, forKey: .id)
}
var id: String
}

Please read the JSON.
Any opening { is quasi a separator. The indentation of the JSON indicates also the hierarchy.
For clarity I removed all coding keys and left the variable names – which should be camelCased – unchanged.
struct Root : Decodable {
let graphql : Graph
// to access the `Media` object declare a lazy instantiated property
lazy var media : Media = {
return graphql.shortcode_media
}()
}
struct Graph : Decodable {
let shortcode_media : Media
}
struct Media : Decodable {
let id: String
let shortcode : String
}
let jsonString = """
{
"graphql": {
"shortcode_media": {
"id": "1657677004214306744",
"shortcode": "BcBQHPchwe4"
}
}
}
"""
do {
let data = Data(jsonString.utf8)
var result = try decoder.decode(Root.self, from: data)
print(result.media)
} catch {
print("error: ", error)
}
Writing a custom initializer with nestedContainer is more effort than creating the actual hierarchy.
Please paste the entire code in a Playground and check it out.

Related

Adding a new property to a model: converting existing JSON

say I had this model:
struct MyModel: Codable {
var name: String
}
By already using this property, I persisted JSON files like this:
{"name":"Foo"}
Now, I want to add a new property uuid, but want to make sure to have a valid migration path to locally persisted existing data. So, I rewrote my struct to this:
struct MyModel {
let uuid: UUID
var name: String
}
extension MyModel: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.uuid = try container.decode(UUID.self, forKey: .uuid)
} catch {
self.uuid = UUID()
}
self.name = try container.decode(String.self, forKey: .name)
}
enum CodingKeys: String, CodingKey {
case uuid
case name
}
}
My thought process here is to catch the error when not finding the property in the JSON by instantiating it manually when needed.
This leads to the error The data couldn’t be read because it is missing..
I might have to add that to be able to load the locally persisted data, I have built the function ATPersistLocally.loadAll(of:) in a personal toolbox package (that I can add to my projects with SPM), that looks like this:
public func loadAll<T: Codable & Identifiable>(of type: T.Type) -> [T] {
var result = [T]()
let pathFolder = docPath.appendingPathComponent(String(describing: type))
if fileManager.fileExists(atPath: pathFolder.path) {
var urls = [URL]()
do {
urls = try fileManager.contentsOfDirectory(at: pathFolder, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
} catch {
ATLogger.shared.logToConsole(message: "Could not load content of directory: \(error.localizedDescription)", type: .error)
}
for url in urls {
if let data = fileManager.contents(atPath: url.path) {
do {
let instance = try decoder.decode(T.self, from: data)
result.append(instance)
} catch {
ATLogger.shared.logToConsole(message: "Could not decode data: \(error.localizedDescription)", type: .error)
}
}
}
}
return result
}
Any hints on how to resolve this?
Simple solution: Omit the CodingKey uuid to decode only name (and you can omit the init method, too).
struct MyModel {
let uuid = UUID()
var name: String
}
extension MyModel: Codable {
enum CodingKeys: String, CodingKey { case name }
}
Important note:
Never print error.localizedDescription in a Decoding catch block, print always only the error instance to get the actual descriptive error message.
Use decodeIfPresentinstead in init(from decoder: Decoder) together with a local variable
if let uuid = try container.decodeIfPresent(UUID.self, forKey: .uuid) {
self.uuid = uuid
} else {
self.uuid = UUID()
}

How should I decode a json object using JSONDecoder if I am unsure of the keys

I have an api response in the following shape -
{
"textEntries":{
"summary":{
"id":"101e9136-efd9-469e-9848-132023d51fb1",
"text":"some text",
"locale":"en_GB"
},
"body":{
"id":"3692b0ec-5b92-4ab1-bc25-7711499901c5",
"text":"some other text",
"locale":"en_GB"
},
"title":{
"id":"45595d27-7e06-491e-890b-f50a5af1cdfe",
"text":"some more text again",
"locale":"en_GB"
}
}
}
I'd like to decode this via JSONDecoder so I can use the properties. The challenge I have is the keys, in this case summary,body and title are generated elsewhere and not always these values, they are always unique, but are based on logic that takes place elsewhere in the product, so another call for a different content article could return leftBody or subTitle etc.
The model for the body of these props is always the same however, I can expect the same fields to exist on any combination of responses.
I will need to be able to access the body of each key in code elsewhere. Another API response will tell me the key I need though.
I am not sure how I can handle this with Decodable as I cannot type the values ahead of time.
I had considered something like modelling the body -
struct ContentArticleTextEntries: Decodable {
var id: String
var text: String
var locale: Locale
}
and storing the values in a struct like -
struct ContentArticle: Decodable {
var textEntries: [String: ContentArticleTextEntries]
private enum CodingKeys: String, CodingKey {
case textEntries
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.textEntries = try values.decode(ContentArticleTextEntries.self, forKey: .textEntries)
}
}
I could them maybe use a subscript elsewhere to access property however I do not know how to decode into this shape as the above would not work.
So I would later access like textEntries["body"] for example.
I also do no know if there is a better way to handle this.
I had considered converting the keys to a 'type' using an enum, but again not knowing the enum cases ahead of time makes this impossible.
I know textEntries this does not change and I know id, text and locale this does not change. It is the keys in between this layer I do not know. I have tried the helpful solution posted by #vadian but cannot seem to make this work in the context of only needing 1 set of keys decoded.
For the proposed solution in this answer the structs are
struct ContentArticleTextEntries: Decodable {
let title : String
let id: String
let text: String
let locale: Locale
enum CodingKeys: String, CodingKey {
case id, text, locale
}
init(from decoder: Decoder) throws {
self.title = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.text = try container.decode(String.self, forKey: .text)
let localeIdentifier = try container.decode(String.self, forKey: .locale)
self.locale = Locale(identifier: localeIdentifier)
}
}
struct ContentArticle: TitleDecodable {
let title : String
var elements: [ContentArticleTextEntries]
}
struct Container: Decodable {
let containers: [ContentArticle]
init(from decoder: Decoder) throws {
self.containers = try decoder.decodeTitledElements(ContentArticle.self)
}
}
Then decode Container.self
If your models are like,
struct ContentArticle: Decodable {
let textEntries: [String: ContentArticleTextEntries]
}
struct ContentArticleTextEntries: Decodable {
var id: String
var text: String
var locale: String
}
Then, you can simply access the data based on key like,
let response = try JSONDecoder().decode(ContentArticle.self, from: data)
let key = "summary"
print(response.textEntries[key])
Note: No need to write enum CodingKeys and init(from:) if there is no special handling while parsing the JSON.
Use "decodeIfPresent" variant method instead of decode method also you need to breakdown the ContentArticleTextEntries dictionary into individual keys:
struct ContentArticle: Decodable {
var id: String
var text: String?
var locale: String?
private enum CodingKeys: String, CodingKey {
case id, text, locale
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? ""
self.text = try container.decodeIfPresent(String.self, forKey: .text)
self.locale = try container.decodeIfPresent(String.self, forKey: .locale)
}
}

Use swift Codable to decode JSON with values as keys

I have a problem decoding a JSON structure which I cannot change to make it easier to decode (it's coming from firebase)..
How do I decode the following JSON into objects?
The problem is how to convert "7E7-M001". It's the name of a container which has drawers. The drawers name is also used as a key.
{
"7E7-M001" : {
"Drawer1" : {
"101" : {
"Partnumber" : "F101"
},
"102" : {
"Partnumber" : "F121"
}
}
},
"7E7-M002": {
"Drawer1": {
"201": {
"Partnumber": "F201"
},
"202": {
"Partnumber": "F221"
}
}
}
}
What do I have to fix in the Container & Drawer class to have the key as a title property and an array of objects in these classes ?
class Container: Codable {
var title: String
var drawers: [Drawer]
}
class Drawer: Codable {
var title: String
var tools: [Tool]
}
class Tool: Codable {
var title: String
var partNumber: String
enum CodingKeys: String, CodingKey {
case partNumber = "Partnumber"
}
}
First I'm going to make some slight simplifications so I can focus on the important points of this question. I'm going to make everything immutable, replace the classes with structs, and only implement Decodable. Making this Encodable is a separate issue.
The central tool for handling unknown value keys is a CodingKey that can handle any string:
struct TitleKey: CodingKey {
let stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
The second important tool is the ability to know your own title. That means asking the decoder "where are we?" That's the last element in the current coding path.
extension Decoder {
func currentTitle() throws -> String {
guard let titleKey = codingPath.last as? TitleKey else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath,
debugDescription: "Not in titled container"))
}
return titleKey.stringValue
}
}
And then we need a way to decode elements that are "titled" this way:
extension Decoder {
func decodeTitledElements<Element: Decodable>(_ type: Element.Type) throws -> [Element] {
let titles = try container(keyedBy: TitleKey.self)
return try titles.allKeys.map { title in
return try titles.decode(Element.self, forKey: title)
}
}
}
With that, we can invent a protocol for these "titled" things and decode them:
protocol TitleDecodable: Decodable {
associatedtype Element: Decodable
init(title: String, elements: [Element])
}
extension TitleDecodable {
init(from decoder: Decoder) throws {
self.init(title: try decoder.currentTitle(),
elements: try decoder.decodeTitledElements(Element.self))
}
}
And that's most of the work. We can use this protocol to make decoding pretty easy for the upper-level layers. Just implement init(title:elements:).
struct Drawer: TitleDecodable {
let title: String
let tools: [Tool]
init(title: String, elements: [Tool]) {
self.title = title
self.tools = elements
}
}
struct Container: TitleDecodable {
let title: String
let drawers: [Drawer]
init(title: String, elements: [Drawer]) {
self.title = title
self.drawers = elements
}
}
Tool is a little different since it's a leaf node and has other things to decode.
struct Tool: Decodable {
let title: String
let partNumber: String
enum CodingKeys: String, CodingKey {
case partNumber = "Partnumber"
}
init(from decoder: Decoder) throws {
self.title = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.partNumber = try container.decode(String.self, forKey: .partNumber)
}
}
That just leaves the very top level. We'll create a Containers type just to wrap things up.
struct Containers: Decodable {
let containers: [Container]
init(from decoder: Decoder) throws {
self.containers = try decoder.decodeTitledElements(Container.self)
}
}
And to use it, decode the top level Containers:
let containers = try JSONDecoder().decode(Containers.self, from: json)
print(containers.containers)
Note that since JSON objects are not order-preserving, the arrays may not be in the same order as the JSON, and may not be in the same order between runs.
Gist
I'm going to extend Rob's answer to give a more general answer and to give it more capabilities. First we'll take an example Json and identify all the scenarios that can be contained within.
let json = Data("""
{
"id": "123456", // id -> primitive data type that can be decoded normally
"name": "Example Name", // name -> primitive data type that can be decoded
"address": { // address -> key => static, object => has static key-value pairs
"city": "Negombo",
"country": "Sri Lanka"
},
"email": { // email -> key => static, object => has only one key-value pair which has a dynamic key. When you're sure, user can have only one email.
"example#gmail.com": { // example#gmail.com -> key => dynamic key, object => in this example the object is
// normal decodable object. But you can have objects that has dynamic key-value pairs.
"verified": true
}
},
"phone_numbers": { // phone_numbers -> key => static, object => has multiple key-value pairs which has a dynamic keys. Assume user can have multiple phone numbers.
"+94772222222": { // +94772222222 -> key => dynamic key, object => in this example the object is
// normal decodable object. But you can have objects that has dynamic key-value pairs.
"isActive": true
},
"+94772222223": { // +94772222223 -> key => another dynamic key, object => another object mapped to dynamic key +94772222223
"isActive": false
}
}
}
""".utf8)
At the end you will be able to read all the values as follows,
let decoder = JSONDecoder()
do {
let userObject = try decoder.decode(UserModel.self, from: json)
print("User ID : \(String(describing: userObject.id))")
print("User Name : \(String(describing: userObject.name))")
print("User Address city : \(String(describing: userObject.address?.city))")
print("User Address country: \(String(describing: userObject.address?.country))")
print("User Email. : \(String(describing: userObject.email?.emailContent?.emailAddress))")
print("User Email Verified : \(String(describing: userObject.email?.emailContent?.verified))")
print("User Phone Number 1 : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers.first?.number))")
print("User Phone Number 2 : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers[1].number))")
print("User Phone Number 1 is Active : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers.first?.isActive))")
print("User Phone Number 2 is Active : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers[1].isActive))")
} catch {
print("Error deserializing JSON: \(error)")
}
So up to address key, you can easily Decode. But after that you're gonna need a specific Object structure to hold all the data mapped by dynamic key-value pairs.
So here is my suggested Swift Object structure. Assume the above Json is for UserModel.
import Foundation
struct UserModel: Decodable {
let id: String
let name: String
let address: Address?
let email: Email?
let phoneNumberDetails: PhoneNumberDetails?
enum CodingKeys: String, CodingKey {
case id
case name
case address
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.address = try? container.decode(Address.self, forKey: .address)
// ["email": Value] -> static key => Email Swift Object
// ["email": Value] -> only object => email.emailContent. Here Value has only one object.
self.email = try decoder.decodeStaticTitledElement(with: TitleKey(stringValue: "email")!, Email.self)
// ["phone_numbers": Value] -> static key => PhoneNumberDetails Swift Object
// ["phone_numbers": Value] -> multiple objects => phoneNumberDetails.phoneNumbers. Here Value has multiples objects.
self.phoneNumberDetails = try decoder.decodeStaticTitledElement(with: TitleKey(stringValue: "phone_numbers")!, PhoneNumberDetails.self)
}
}
struct Address: Decodable {
let city: String
let country: String
enum CodingKeys: String, CodingKey {
case city
case country
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.city = try container.decode(String.self, forKey: .city)
self.country = try container.decode(String.self, forKey: .country)
}
}
/*
* Extends SingleTitleDecodable.
* Object that was mapped to static key "email".
* SingleTitleDecodable uses when you know the Parent object has only one dynamic key-value pair
* In this case Parent object is "email" object in the json, and "example#gmail.com": { body } is the only dynamic key-value pair
* key-value pair is mapped into EmailContent
*/
struct Email: SingleTitleDecodable {
let emailContent: EmailContent?
init(title: String, element: EmailContent?) {
self.emailContent = element
}
}
struct EmailContent: Decodable {
let emailAddress: String
let verified: Bool
enum CodingKeys: String, CodingKey {
case verified
}
init(from decoder: Decoder) throws {
self.emailAddress = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.verified = try container.decode(Bool.self, forKey: .verified)
}
}
/*
* Extends TitleDecodable.
* Object that was mapped to static key "phone_numbers".
* TitleDecodable uses when you know the Parent object has multiple dynamic key-value pair
* In this case Parent object is "phone_numbers" object in the json, and "+94772222222": { body }, "+94772222222": { body } are the multiple dynamic key-value pairs
* Multiple dynamic key-value pair are mapped into PhoneNumber array
*/
struct PhoneNumberDetails: TitleDecodable {
let phoneNumbers: [PhoneNumber]
init(title: String, elements: [PhoneNumber]) {
self.phoneNumbers = elements
}
}
struct PhoneNumber: Decodable {
let number: String
let isActive: Bool
enum CodingKeys: String, CodingKey {
case isActive
}
init(from decoder: Decoder) throws {
self.number = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.isActive = try container.decode(Bool.self, forKey: .isActive)
}
}
Focus on how the Json has transformed into the Object structure. Here is the mechanism extracted and improved from Rob's answer.
import Foundation
/*
* This is to handle unknown keys.
* Convert Keys with any String value to CodingKeys
*/
struct TitleKey: CodingKey {
let stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
extension Decoder {
/*
* Decode map into object array that is type of Element
* [Key: Element] -> [Element]
* This will be used when the keys are dynamic and have multiple keys
* Within type Element we can embed relevant Key using => 'try decoder.currentTitle()'
* So you can access Key using => 'element.key'
*/
func decodeMultipleDynamicTitledElements<Element: Decodable>(_ type: Element.Type) throws -> [Element] {
var decodables: [Element] = []
let titles = try container(keyedBy: TitleKey.self)
for title in titles.allKeys {
if let element = try? titles.decode(Element.self, forKey: title) {
decodables.append(element)
}
}
return decodables
}
/*
* Decode map into optional object that is type of Element
* [Key: Element] -> Element?
* This will be used when the keys are dynamic and when you're sure there'll be only one key-value pair
* Within type Element we can embed relevant Key using => 'try decoder.currentTitle()'
* So you can access Key using => 'element.key'
*/
func decodeSingleDynamicTitledElement<Element: Decodable>(_ type: Element.Type) throws -> Element? {
let titles = try container(keyedBy: TitleKey.self)
for title in titles.allKeys {
if let element = try? titles.decode(Element.self, forKey: title) {
return element
}
}
return nil
}
/*
* Decode map key-value pair into optional object that is type of Element
* Key: Element -> Element?
* This will be used when the root key is known, But the value is constructed with Maps where the keys can be Unknown
*/
func decodeStaticTitledElement<Element: Decodable>(with key: TitleKey, _ type: Element.Type) throws -> Element? {
let titles = try container(keyedBy: TitleKey.self)
if let element = try? titles.decode(Element.self, forKey: key) {
return element
}
return nil
}
/*
* This will be used to know where the Element is in the Object tree
* Returns the Key of the Element which was mapped to
*/
func currentTitle() throws -> String {
guard let titleKey = codingPath.last as? TitleKey else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Not in titled container"))
}
return titleKey.stringValue
}
}
/*
* Class that implements this Protocol, contains an array of Element Objects,
* that will be mapped from a 'Key1: [Key2: Element]' type of map.
* This will be used when the Key2 is dynamic and have multiple Key2 values
* Key1 -> Key1: TitleDecodable
* [Key2: Element] -> Key1_instance.elements
* Key2 -> Key1_instance.elements[index].key2
*/
protocol TitleDecodable: Decodable {
associatedtype Element: Decodable
init(title: String, elements: [Element])
}
extension TitleDecodable {
init(from decoder: Decoder) throws {
self.init(title: try decoder.currentTitle(), elements: try decoder.decodeMultipleDynamicTitledElements(Element.self))
}
}
/*
* Class that implements this Protocol, contains a variable which is type of Element,
* that will be mapped from a 'Key1: [Key2: Element]' type of map.
* This will be used when the Keys2 is dynamic and have only one Key2-value pair
* Key1 -> Key1: SingleTitleDecodable
* [Key2: Element] -> Key1_instance.element
* Key2 -> Key1_instance.element.key2
*/
protocol SingleTitleDecodable: Decodable {
associatedtype Element: Decodable
init(title: String, element: Element?)
}
extension SingleTitleDecodable {
init(from decoder: Decoder) throws {
self.init(title: try decoder.currentTitle(), element: try decoder.decodeSingleDynamicTitledElement(Element.self))
}
}
In this case we can't create static codable classes for this JSON.
Better use JSON serialization and retrive it.

parse JSON with swift via a model to object - decode int/string trouble

To get JSON from a website and turn it into an object is fairly simple using swift 4:
class func getJSON(completionHandler: #escaping (MyModel) -> Void)
{
let myUrl = "https://example.com/whatever"
if let myUrl = URL(string: myUrl)
{
URLSession.shared.dataTask(with: myUrl)
{ (data, response, err) in
if let data = data
{
do
{
let myData = try JSONDecoder().decode(MyModel.self, from: data)
completionHandler(myData)
}
catch let JSONerr
{
print("Error: \(JSONerr)")
}
}
return
}.resume()
}
}
MyModel is a data model:
struct MyModel
{
products: [MyProduct]
}
struct MyProduct
{
id: Int
...
I use this to GET from my WebService and it works well for most JSON structures.
However, I facing trouble with this complex JSON object. (By complex I mean too long to post here, so I hope you can figure-out such a pattern. Also the JSON object it has many nested arrays, etc.)
Eg.
{
"products" : [
{
"name" : "abc"
"colors" : [
{
"outside" : [
{
"main" : "blue"
}]
}]
"id" :
...
},
{
"name" : "xyzzy"
"colors" : [
{
"outside" : [
{
"main" : "blue"
}]
}]
"id" :
...
}]
}
(This may not be valid JSON - it is a simple extract of a larger part.)
The app crashes "...Expected to decode String but found a number
instead."!
So I change the model to use a 'String' in place of the
'Int'.
Again it crashes, saying "...Expected to decode Int but found
a string/data instead."!
So I change the model back, place an 'Int'
in place of the 'String'.
(The cycle repeats.)
It seems the value in question is sometimes an Int and sometimes a
String.
This NOT only happens with a certain key. I know of at least five other similar cases in this JSON.
So that means that I may get another error for another key, if a solution was only for that specific key. I would not be surprised to find many other cases as well.
QUESTION: How can I properly decode the JSON to my object, where the type of its elements can be either an Int or a String?
I want a solution that will either apply to all Model members or try convert a value to a String type if Int fails. Since I don't know which other keys will as fail.
You can use if lets to handle unpredictable values:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
if let memberValue = try? container.decode([String].self, forKey: .member){
stringArrayMember = memberValue
}
else if let str = try? container.decode(String.self, forKey: .member){
stringMember = str
}
else if let int = try? container.decode(Int.self, forKey: .member){
intMember = int
}
}
Or if it's a specific case of String vs Int and you'd like the same variable to handle the values, then something like:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
if let str = try? container.decode(String.self, forKey: .member){
stringMember = str
}
else if let int = try? container.decode(Int.self, forKey: .member){
stringMember = String(int)
}
}
Edit
Your MyProduct will now look like:
struct MyProduct: Decodable {
var id: String?
var someOtherProperty: String?
enum MemberKeys: String, CodingKey {
case id
case someOtherProperty
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
someOtherProperty = try? container.decode(String.self, forKey: .someOtherProperty)
// Problematic property which can be String/Int
if let str = try? container.decode(String.self, forKey: .id){
id = str
}
else if let int = try? container.decode(Int.self, forKey: .id){
id = String(int)
}
}
}
Hope this helps.
This wasn't the problem that the error message gave!
All I needed to do to fix the problem was to employ CodingKeys.
I was hoping to avoid this since the data structure (JSON) had lots of members. But this fixed the problem.
Now an example of my model:
struct Product
{
let name: String
let long_name_value: String
...
enum MemberKeys: String, CodingKey
{
case name
case longNameValue = "long_name_value"
...
}
}
I guess the reason is swift doesn't like snake case (eg. "long_name_value"), so I needed to convert it to camel case (eg."longNameValue"). Then the errors disappeared.

Type Mismatch error parsing JSON with Swift Decodable

When I try to decode JSON I get the error:
(Error: typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [sweetyanime.Video.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).video, sweetyanime.iD.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).ID, sweetyanime.other.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).CountOfVideos], debugDescription: "Expected to decode String but found a dictionary instead.", underlyingError: nil)
1) JSON:
{ "video":{
"ID":{
"Name":"NameOfAnime",
"Describe":"SomeDescribe",
"Image":"https://firebasestorage.googleapis.com/v0/b/sweety-anime-e6bb4.appspot.com/o/main.png?alt=media&token=042a2dad-8519-4904-9ba3-262c2c962434",
"CountOfVideos":{
"1Series":"https://firebasestorage.googleapis.com/v0/b/sweety-anime-e6bb4.appspot.com/o/message_movies%252F12323439-9729-4941-BA07-2BAE970967C7.movalt=media&token=978d8b3a-7aad-468f-87d4-2b587d616720"
}
} } }
2) Swift code:
let jsonUrl = "file:///Users/tima/WebstormProjects/untitled/db.json"
guard let url = URL(string: jsonUrl) else { return }
URLSession.shared.dataTask(with: url) { (data, reponse, error) in
guard let data = data else {return}
do {
let video = try
JSONDecoder().decode(Video.self, from: data)
print(video.video.ID.Name)
} catch let jsonErr {
print("Error: ", jsonErr)
}
}.resume()
3) Video.swift
struct Video: Decodable {
private enum CodingKeys : String, CodingKey { case video = "video"
}
let video: iD
}
struct iD: Decodable {
private enum CodingKeys : String, CodingKey { case ID = "ID" }
let ID: other
}
struct other: Decodable {
private enum CodingKeys : String, CodingKey {
case Name = "Name"
case Describe = "Describe"
case Image = "Image"
case CountOfVideos = "CountOfVideos"
}
let Name: String
let Describe: String
let Image: String
let CountOfVideos: String
}
Let's put some line breaks in your error message to make it understandable:
(Error: typeMismatch(Swift.String,
Swift.DecodingError.Context(codingPath: [
sweetyanime.Video.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).video,
sweetyanime.iD.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).ID,
sweetyanime.other.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).CountOfVideos],
debugDescription: "Expected to decode String but found a dictionary instead.",
underlyingError: nil)
So it was trying to decode the value for “CountOfVideos”. It expected a String but it found a dictionary.
You need to either define a struct corresponding to the “CountOfVideos” dictionary (it appears to contain one key, “1Series”, with a string value), or you need to remove the CountOfVideos property from your other struct so you don't try to decode it at all.
My I think you should implement the init(from decoder) initializer that is part of Decodable. It gives a lot more flexibility on how to handle nested JSON than you get from the automatic implementation. One possible implementation would be something like:
struct Video: Decodable {
let name: String
let describe: String
let image: String
let countOfVideos: String
private enum VideoKey: String, CodingKey {
case video = "video"
}
private enum IDKey: String, CodingKey {
case id = "ID"
}
private enum CodingKeys: String, CodingKey {
case name = "Name"
case describe = "Describe"
case image = "Image"
case countOfVideos = "CountOfVideos"
}
private enum VideoCountKey: String, CodingKey {
case videoCount = "1Series"
}
init(from decoder: Decoder) throws {
let videoContainer = try decoder.container(keyedBy: VideoKey.self)
let idContainer = try videoContainer.nestedContainer(keyedBy: IDKey.self, forKey: .video)
let valuesContainer = try idContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .id)
self.name = try valuesContainer.decode(String.self, forKey: .name)
self.describe = try valuesContainer.decode(String.self, forKey: .describe)
self.image = try valuesContainer.decode(String.self, forKey: .image)
let videoCountContainer = try valuesContainer.nestedContainer(keyedBy: VideoCountKey.self, forKey: .countOfVideos)
self.countOfVideos = try videoCountContainer.decode(String.self, forKey: .videoCount)
}
This works with your example data in a Playground. I really don't know if you wanted the key or the value for your videoCount. (Neither looks like a count of videos to me. Generally, you just need to define a CodingKey enum for every level of your nested JSON and you can decode individual values as you see fit. (Note, since I've only seen one example of your data, this may break on some stuff in whatever API you're working with, so modify/take the ideas and rewrite it as needed)