Swift 4 Decodable - Additional Variables - json

Something I havent figured out or have been able to find online as of yet.
Is there a way to add additional fields onto a struct containing the decodable protocol in which are not present in the JSON Data?
For example and simplicity, say I have an array of json objects structured as such
{
"name": "name1",
"url": "www.google.com/randomImage"
}
but say I want to add a UIImage variable to that struct containing the decodable such as
struct Example1: Decodable {
var name: String?
var url: String?
var urlImage: UIImage? //To add later
}
Is there a way to implement the decodable protocol in order to get the name and url from the JSON but allow me to add the UIImage later?

To exclude urlImage you must manually conform to Decodable instead of letting its requirements be synthesized:
struct Example1 : Decodable { //(types should be capitalized)
var name: String?
var url: URL? //try the `URL` type! it's codable and much better than `String` for storing URLs
var urlImage: UIImage? //not decoded
private enum CodingKeys: String, CodingKey { case name, url } //this is usually synthesized, but we have to define it ourselves to exclude `urlImage`
}
Before Swift 4.1 this only works if you add = nil to urlImage, even though the default value of nil is usually assumed for optional properties.
If you want to provide a value for urlImage at initialization, rather than using = nil, you can also manually define the initializer:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
url = try container.decode(URL.self, forKey: .url)
urlImage = //whatever you want!
}

Actually, I'd make urlImage a lazy var. That way you don't have to worry about modifying the coding keys. All you have to do is write your getter, like so...
struct Example1 : Decodable {
var name : String?
var url : URL? // Better than String
lazy var urlImage: UIImage? = {
// Put your image-loading code here using 'url'
}()
}

Related

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)
}
}

Umlaut in URL fails Codable (Swift) - what to do?

Using Swift-4.1, Xcode-9.3.1, iOS-11.3.1
I use the Codable protocol to decode a JSON-file. Everything works, except until the moment where I have an Internationalised Domain-Name (in this case, with a German Umlaut "ä") in a URL (example: http://www.rhätische-zeitung.ch).
This leads to a decoder-error inside the following code:
func loadJSON(url: URL) -> Media? {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let media = try decoder.decode(Media.self, from: data)
return media
} catch {
print("error:\(error)")
}
return nil
}
The error-message is:
The Codable protocol does not seem to be able to decode this URL form
my JSON-file into the needed Struct.
Here is the Struct:
struct Media: Codable {
var publisher: [MediaPublisher]
}
struct MediaPublisher: Codable {
var title: String?
var homepage_url: URL?
}
And here is the JSON-excerpt:
{
"publisher": [
{
"title" : "Rhätische-Zeitung",
"homepage_url" : "http://www.rhätische-zeitung.ch",
}
]
}
Since the JSON-file is coming from the outside, I have no control over the content. And therefore, replacing the URL inside the JSON is not an option !
(Therefore, I cannot replace the URL inside the JSON to an accepted Internationalized Form suche as: www.xn--rhtische-zeitung-wnb.ch) !!
I know that there are techniques to place a custom initialiser into the Struct-definition (see my trials below...) - but since new to Codable, I don't know how to do that for this current URL-Umlaut problem. The custom-initialiser I placed below does return nil for the URL at question. What do I need to change ??
Or is there another way of making this JSON-decoding of an URL with Umlaut work ??
Here is the Struct, this time with a custom initialiser:
(at least with this, I can get rid of the error-message above... But the URL is now nil it seems and that is not what I want either)
struct Media: Codable {
var publisher: [MediaPublisher]
}
struct MediaPublisher: Codable {
var title: String?
var homepage_url: URL?
// default initializer
init(title: String?, homepage_url: URL?) {
self.title = title
self.homepage_url = homepage_url
}
// custom initializer
init(from decoder: Decoder) throws {
let map = try decoder.container(keyedBy: CodingKeys.self)
self.title = try? map.decode(String.self, forKey: .title)
self.homepage_url = try? map.decode(URL.self, forKey: .homepage_url)
}
private enum CodingKeys: CodingKey {
case title
case homepage_url
}
}
I just stumbled over the same problem. The solution is actually fairly simple and works well in my brief testing.
The idea is to not let the decoder decode the value as an URL, because it expects the string behind it to be in of a certain format. What we can do to circumvent this is to decode the value as a string directly and convert that manually into a URL.
I wrote a little extension that does that job for me:
extension KeyedDecodingContainer {
/// Decodes the string at the given key as a URL. This allows for special characters like umlauts to be decoded correctly.
func decodeSanitizedURL(forKey key: KeyedDecodingContainer<K>.Key) throws -> URL {
let urlString = try self.decode(String.self, forKey: key)
// Sanitize string and attempt to convert it into a valid url
if let urlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: urlString) {
return url
}
// Throw an error as the URL could not be decoded
else {
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Could not decode \(urlString)")
}
}
}
This allows for a streamlined use in the init method.
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.url = try values.decodeSanitizedURL(forKey: .url)
}
Hope that helps, even if the question is a little bit older.

Swift 4 Codable - Bool or String values

Looking for some input as to how you would handle the scenario I recently ran into.
I have been using Swift 4s Codable with success but today noticed a crash that I didn't expect. The API that I am working with, says it returns a boolean for the key manage_stock.
My stubbed struct looks like:
struct Product: Codable {
var manage_stock: Bool?
}
That works fine, the problem is that the API sometimes returns a string instead of a boolean.
Because of this, my decode fails with:
Expected to decode Bool but found a string/data instead.
The string only ever equals "parent" and I want it to equate to false.
I am also fine with changing my struct to var manage_stock: String? if that makes things easier to bring the JSON data in from the API. But of course, if I change that, my error just changes to:
Expected to decode String but found a number instead.
Is there a simple way to handle this mutation or will I need to lose all the automation that Codable brings to the table and implement my own init(decoder: Decoder).
Cheers
Since you can't always be in control of the APIs you're working with, here's one simple way to solve this with Codable directly, by overriding init(from:):
struct Product : Decodable {
// Properties in Swift are camelCased. You can provide the key separately from the property.
var manageStock: Bool?
private enum CodingKeys : String, CodingKey {
case manageStock = "manage_stock"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.manageStock = try container.decodeIfPresent(Bool.self, forKey: .manageStock)
} catch DecodingError.typeMismatch {
// There was something for the "manage_stock" key, but it wasn't a boolean value. Try a string.
if let string = try container.decodeIfPresent(String.self, forKey: .manageStock) {
// Can check for "parent" specifically if you want.
self.manageStock = false
}
}
}
}
See Encoding and Decoding Custom Types for more info on this.

How can I implement polymorphic decoding of JSON data in Swift 4?

I am attempting to render a view from data returned from an API endpoint. My JSON looks (roughly) like this:
{
"sections": [
{
"title": "Featured",
"section_layout_type": "featured_panels",
"section_items": [
{
"item_type": "foo",
"id": 3,
"title": "Bisbee1",
"audio_url": "http://example.com/foo1.mp3",
"feature_image_url" : "http://example.com/feature1.jpg"
},
{
"item_type": "bar",
"id": 4,
"title": "Mortar8",
"video_url": "http://example.com/video.mp4",
"director" : "John Smith",
"feature_image_url" : "http://example.com/feature2.jpg"
}
]
}
]
}
I have an object that represents how to layout a view in my UI. It looks like this:
public struct ViewLayoutSection : Codable {
var title: String = ""
var sectionLayoutType: String
var sectionItems: [ViewLayoutSectionItemable] = []
}
ViewLayoutSectionItemable is a protocol that includes, among other things, a title and a URL to an image to use in the layout.
However, the sectionItems array is actually made up of different types. What I'd like to do is instantiate each section item as an instance of its own class.
How do I setup the init(from decoder: Decoder) method for the ViewLayoutSection to let me iterate over the items in that JSON array and create an instance of the proper class in each case?
Polymorphic design is a good thing: many design patterns exhibit polymorphism to make the overall system more flexible and extensible.
Unfortunately, Codable doesn't have "built in" support for polymorphism, at least not yet.... there's also discussion about whether this is actually a feature or a bug.
Fortunately, you can pretty easily create polymorphic objects using an enum as an intermediate "wrapper."
First, I'd recommend declaring itemType as a static property, instead of an instance property, to make switching on it easier later. Thereby, your protocol and polymorphic types would look like this:
import Foundation
public protocol ViewLayoutSectionItemable: Decodable {
static var itemType: String { get }
var id: Int { get }
var title: String { get set }
var imageURL: URL { get set }
}
public struct Foo: ViewLayoutSectionItemable {
// ViewLayoutSectionItemable Properties
public static var itemType: String { return "foo" }
public let id: Int
public var title: String
public var imageURL: URL
// Foo Properties
public var audioURL: URL
}
public struct Bar: ViewLayoutSectionItemable {
// ViewLayoutSectionItemable Properties
public static var itemType: String { return "bar" }
public let id: Int
public var title: String
public var imageURL: URL
// Bar Properties
public var director: String
public var videoURL: URL
}
Next, create an enum for the "wrapper":
public enum ItemableWrapper: Decodable {
// 1. Keys
fileprivate enum Keys: String, CodingKey {
case itemType = "item_type"
case sections
case sectionItems = "section_items"
}
// 2. Cases
case foo(Foo)
case bar(Bar)
// 3. Computed Properties
public var item: ViewLayoutSectionItemable {
switch self {
case .foo(let item): return item
case .bar(let item): return item
}
}
// 4. Static Methods
public static func items(from decoder: Decoder) -> [ViewLayoutSectionItemable] {
guard let container = try? decoder.container(keyedBy: Keys.self),
var sectionItems = try? container.nestedUnkeyedContainer(forKey: .sectionItems) else {
return []
}
var items: [ViewLayoutSectionItemable] = []
while !sectionItems.isAtEnd {
guard let wrapper = try? sectionItems.decode(ItemableWrapper.self) else { continue }
items.append(wrapper.item)
}
return items
}
// 5. Decodable
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
let itemType = try container.decode(String.self, forKey: Keys.itemType)
switch itemType {
case Foo.itemType: self = .foo(try Foo(from: decoder))
case Bar.itemType: self = .bar(try Bar(from: decoder))
default:
throw DecodingError.dataCorruptedError(forKey: .itemType,
in: container,
debugDescription: "Unhandled item type: \(itemType)")
}
}
}
Here's what the above does:
You declare Keys that are relevant to the response's structure. In your given API, you're interested in sections and sectionItems. You also need to know which key represents the type, which you declare here as itemType.
You then explicitly list every possible case: this violates the Open Closed Principle, but this is "okay" to do as it's acting as a "factory" for creating items....
Essentially, you'll only have this ONCE throughout your entire app, just right here.
You declare a computed property for item: this way, you can unwrap the underlying ViewLayoutSectionItemable without needing to care about the actual case.
This is the heart of the "wrapper" factory: you declare items(from:) as a static method that's capable of returning [ViewLayoutSectionItemable], which is exactly what you want to do: pass in a Decoder and get back an array containing polymorphic types! This is the method you'll actually use instead of decoding Foo, Bar or any other polymorphic arrays of these types directly.
Lastly, you must make ItemableWrapper implement the Decodable method. The trick here is that ItemWrapper always decodes an ItemWrapper: thereby, this works how Decodable is expecting.
As it's an enum, however, it's allowed to have associated types, which is exactly what you do for each case. Hence, you can indirectly create polymorphic types!
Since you've done all the heavy lifting within ItemWrapper, it's very easy to now go from a Decoder to an `[ViewLayoutSectionItemable], which you'd do simply like this:
let decoder = ... // however you created it
let items = ItemableWrapper.items(from: decoder)
A simpler version of #CodeDifferent's response, which addresses #JRG-Developer's comment. There is no need to rethink your JSON API; this is a common scenario. For each new ViewLayoutSectionItem you create, you only need to add one case and one line of code to the PartiallyDecodedItem.ItemKind enum and PartiallyDecodedItem.init(from:) method respectively.
This is not only the least amount of code compared to the accepted answer, it is more performant. In #CodeDifferent's option, you are required to initialize 2 arrays with 2 different representations of the data to get your array of ViewLayoutSectionItems. In this option, you still need to initialize 2 arrays, but get to only have one representation of the data by taking advantage of copy-on-write semantics.
Also note that it is not necessary to include ItemType in the protocol or the adopting structs (it doesn't make sense to include a string describing what type a type is in a statically typed language).
protocol ViewLayoutSectionItem {
var id: Int { get }
var title: String { get }
var imageURL: URL { get }
}
struct Foo: ViewLayoutSectionItem {
let id: Int
let title: String
let imageURL: URL
let audioURL: URL
}
struct Bar: ViewLayoutSectionItem {
let id: Int
let title: String
let imageURL: URL
let videoURL: URL
let director: String
}
private struct PartiallyDecodedItem: Decodable {
enum ItemKind: String, Decodable {
case foo, bar
}
let kind: Kind
let item: ViewLayoutSectionItem
private enum DecodingKeys: String, CodingKey {
case kind = "itemType"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DecodingKeys.self)
self.kind = try container.decode(Kind.self, forKey: .kind)
self.item = try {
switch kind {
case .foo: return try Foo(from: decoder)
case .number: return try Bar(from: decoder)
}()
}
}
struct ViewLayoutSection: Decodable {
let title: String
let sectionLayoutType: String
let sectionItems: [ViewLayoutSectionItem]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.title = try container.decode(String.self, forKey: .title)
self.sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
self.sectionItems = try container.decode([PartiallyDecodedItem].self, forKey: .sectionItems)
.map { $0.item }
}
}
To handle the snake case -> camel case conversion, rather than manually type out all of the keys, you can simply set a property on JSONDecoder
struct Sections: Decodable {
let sections: [ViewLayoutSection]
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let sections = try decode(Sections.self, from: json)
.sections
I recommend you to be judicious on the use of Codable. If you only want to decode a type from JSON and not encode it, conforming it to Decodable alone is enough. And since you have already discovered that you need to decode it manually (via a custom implementation of init(from decoder: Decoder)), the question becomes: what is the least painful way to do it?
First, the data model. Note that ViewLayoutSectionItemable and its adopters do not conform to Decodable:
enum ItemType: String, Decodable {
case foo
case bar
}
protocol ViewLayoutSectionItemable {
var id: Int { get }
var itemType: ItemType { get }
var title: String { get set }
var imageURL: URL { get set }
}
struct Foo: ViewLayoutSectionItemable {
let id: Int
let itemType: ItemType
var title: String
var imageURL: URL
// Custom properties of Foo
var audioURL: URL
}
struct Bar: ViewLayoutSectionItemable {
let id: Int
let itemType: ItemType
var title: String
var imageURL: URL
// Custom properties of Bar
var videoURL: URL
var director: String
}
Next, here's how we will decode the JSON:
struct Sections: Decodable {
var sections: [ViewLayoutSection]
}
struct ViewLayoutSection: Decodable {
var title: String = ""
var sectionLayoutType: String
var sectionItems: [ViewLayoutSectionItemable] = []
// This struct use snake_case to match the JSON so we don't have to provide a custom
// CodingKeys enum. And since it's private, outside code will never see it
private struct GenericItem: Decodable {
let id: Int
let item_type: ItemType
var title: String
var feature_image_url: URL
// Custom properties of all possible types. Note that they are all optionals
var audio_url: URL?
var video_url: URL?
var director: String?
}
private enum CodingKeys: String, CodingKey {
case title
case sectionLayoutType = "section_layout_type"
case sectionItems = "section_items"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
sectionItems = try container.decode([GenericItem].self, forKey: .sectionItems).map { item in
switch item.item_type {
case .foo:
// It's OK to force unwrap here because we already
// know what type the item object is
return Foo(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, audioURL: item.audio_url!)
case .bar:
return Bar(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, videoURL: item.video_url!, director: item.director!)
}
}
}
Usage:
let sections = try JSONDecoder().decode(Sections.self, from: json).sections
I have written a blog post about this exact problem.
In summary. I suggest defining an extension on Decoder
extension Decoder {
func decode<ExpectedType>(_ expectedType: ExpectedType.Type) throws -> ExpectedType {
let container = try self.container(keyedBy: PolymorphicMetaContainerKeys.self)
let typeID = try container.decode(String.self, forKey: .itemType)
guard let types = self.userInfo[.polymorphicTypes] as? [Polymorphic.Type] else {
throw PolymorphicCodableError.missingPolymorphicTypes
}
let matchingType = types.first { type in
type.id == typeID
}
guard let matchingType = matchingType else {
throw PolymorphicCodableError.unableToFindPolymorphicType(typeID)
}
let decoded = try matchingType.init(from: self)
guard let decoded = decoded as? ExpectedType else {
throw PolymorphicCodableError.unableToCast(
decoded: decoded,
into: String(describing: ExpectedType.self)
)
}
return decoded
}
}
Then adding the possible polymorphic types to the Decoder instance:
var decoder = JSONDecoder()
decoder.userInfo[.polymorphicTypes] = [
Snake.self,
Dog.self
]
If you have nested polymeric values you can write a property wrapper to that calls this decode method so that you do not need to define custom init(from:).
Here's a small utility package that resolve this exact problem.
It was built around a configuration type that has variants for the decodable type defines the type information discriminator.
enum DrinkFamily: String, ClassFamily {
case drink = "drink"
case beer = "beer"
static var discriminator: Discriminator = .type
typealias BaseType = Drink
func getType() -> Drink.Type {
switch self {
case .beer:
return Beer.self
case .drink:
return Drink.self
}
}
}
Later in your collection overload the init method to use our KeyedDecodingContainer extension.
class Bar: Decodable {
let drinks: [Drink]
private enum CodingKeys: String, CodingKey {
case drinks
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
drinks = try container.decodeHeterogeneousArray(OfFamily: DrinkFamily.self, forKey: .drinks)
}
}

How to exclude properties from Swift Codable?

Swift's Encodable/Decodable protocols, released with Swift 4, make JSON (de)serialization quite pleasant. However, I have not yet found a way to have fine-grained control over which properties should be encoded and which should get decoded.
I have noticed that excluding the property from the accompanying CodingKeys enum excludes the property from the process altogether, but is there a way to have more fine-grained control?
The list of keys to encode/decode is controlled by a type called CodingKeys (note the s at the end). The compiler can synthesize this for you but can always override that.
Let's say you want to exclude the property nickname from both encoding and decoding:
struct Person: Codable {
var firstName: String
var lastName: String
var nickname: String?
private enum CodingKeys: String, CodingKey {
case firstName, lastName
}
}
If you want it to be asymmetric (i.e. encode but not decode or vice versa), you have to provide your own implementations of encode(with encoder: ) and init(from decoder: ):
struct Person: Codable {
var firstName: String
var lastName: String
// Since fullName is a computed property, it's excluded by default
var fullName: String {
return firstName + " " + lastName
}
private enum CodingKeys: String, CodingKey {
case firstName, lastName, fullName
}
// We don't want to decode `fullName` from the JSON
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
}
// But we want to store `fullName` in the JSON anyhow
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
try container.encode(fullName, forKey: .fullName)
}
}
Solution with custom property wrapper
struct Person: Codable {
var firstName: String
var lastName: String
#CodableIgnored
var nickname: String?
}
Where CodableIgnored is
#propertyWrapper
public struct CodableIgnored<T>: Codable {
public var wrappedValue: T?
public init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}
public init(from decoder: Decoder) throws {
self.wrappedValue = nil
}
public func encode(to encoder: Encoder) throws {
// Do nothing
}
}
extension KeyedDecodingContainer {
public func decode<T>(
_ type: CodableIgnored<T>.Type,
forKey key: Self.Key) throws -> CodableIgnored<T>
{
return CodableIgnored(wrappedValue: nil)
}
}
extension KeyedEncodingContainer {
public mutating func encode<T>(
_ value: CodableIgnored<T>,
forKey key: KeyedEncodingContainer<K>.Key) throws
{
// Do nothing
}
}
Another way to exclude some properties from encoder, separate coding container can be used
struct Person: Codable {
let firstName: String
let lastName: String
let excludedFromEncoder: String
private enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
private enum AdditionalCodingKeys: String, CodingKey {
case excludedFromEncoder
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let anotherContainer = try decoder.container(keyedBy: AdditionalCodingKeys.self)
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
excludedFromEncoder = try anotherContainer(String.self, forKey: . excludedFromEncoder)
}
// it is not necessary to implement custom encoding
// func encode(to encoder: Encoder) throws
// let person = Person(firstName: "fname", lastName: "lname", excludedFromEncoder: "only for decoding")
// let jsonData = try JSONEncoder().encode(person)
// let jsonString = String(data: jsonData, encoding: .utf8)
// jsonString --> {"firstName": "fname", "lastName": "lname"}
}
same approach can be used for decoder
If we need to exclude decoding of a couple of properties from a large set of properties in the structure, declare them as optional properties. Code to unwrapping optionals is less than writing a lot of keys under CodingKey enum.
I would recommend using extensions to add computed instance properties and computed type properties. It separates codable comforming properties from other logic hence provides better readability.
You can use computed properties:
struct Person: Codable {
var firstName: String
var lastName: String
var nickname: String?
var nick: String {
get {
nickname ?? ""
}
}
private enum CodingKeys: String, CodingKey {
case firstName, lastName
}
}
While this can be done it ultimately ends up being very unSwifty and even unJSONy. I think I see where you are coming from, the concept of #ids is prevalent in HTML, but it is rarely transported over to the world of JSON which I consider a good thing (TM).
Some Codable structs will be able to parse your JSON file just fine if you restructure it using recursive hashes, i.e. if your recipe just contains an array of ingredients which in turn contains (one or several) ingredient_info. That way the parser will help you to stitch your network together in the first place and you only have to provide some backlinks through a simple traversal the structure if you really need them. Since this requires a thorough rework of your JSONand your data structure I only sketch out the idea for you to think about it. If you deem it acceptable please tell me in the comments then I could elaborate it further, but depending on the circumstances you may not be at the liberty to change either one of them.
I have used protocol and its extension along with AssociatedObject to set and get image (or any property which needs to be excluded from Codable) property.
With this we dont have to implement our own Encoder and Decoder
Here is the code, keeping relevant code for simplicity:
protocol SCAttachmentModelProtocol{
var image:UIImage? {get set}
var anotherProperty:Int {get set}
}
extension SCAttachmentModelProtocol where Self: SCAttachmentUploadRequestModel{
var image:UIImage? {
set{
//Use associated object property to set it
}
get{
//Use associated object property to get it
}
}
}
class SCAttachmentUploadRequestModel : SCAttachmentModelProtocol, Codable{
var anotherProperty:Int
}
Now, whenever we want to access the Image property we can use on the object confirming to protocol (SCAttachmentModelProtocol)