I'm using a protocol to create several structs which I use to decode using JSONDecoder. Here's a code sample of what I'm trying to achieve.
protocol Animal: Codable
{
var name: String { get }
var age: Int { get }
}
struct Dog: Animal
{
let name: String
let age: Int
let type: String
}
struct Cat: Animal
{
let name: String
let age: Int
let color: String
}
Here are the seperate JSON payloads of dog and cat:
{
"name": "fleabag",
"age": 3,
"type": "big"
}
{
"name": "felix",
"age": 2,
"color": "black"
}
So when I decode the JSON, I'm not sure what JSON I'll have, dog or cat. I tried doing this:
let data = Data(contentsOf: url)
let value = JSONDecoder().decode(Animal.self, from: data)
But end up with this error:
In argument type 'Animal.Protocol', 'Animal' does not conform to expected type 'Decodable'
Any ideas as to the best approach to parse either dog or cat returning an instance of Animal?
Thanks
You're not going to be able to use this:
let animal = try? JSONDecoder().decode(Animal.self, from: data)
To decode a Dog or a Cat. It's always going to be an Animal.
If you want to decode both those JSON objects to Animal, then define Animal like this:
struct Animal: Codable {
var name: String
var age: Int
}
Of course, you'll lose the distinctive elements that make them a Dog (type) or Cat (color).
Your opening a somewhat ugly can of worms here. I understand what you try to do, but unfortunately it fails in a number of ways. You can get somewhat close to what you want with the following Playground:
import Cocoa
let dogData = """
{
"name": "fleabag",
"age": 3,
"type": "big"
}
""".data(using: .utf8)!
let catData = """
{
"name": "felix",
"age": 2,
"color": "black"
}
""".data(using: .utf8)!
protocol Animal: Codable
{
var name: String { get }
var age: Int { get }
}
struct Dog: Animal
{
let name: String
let age: Int
let type: String
}
struct Cat: Animal
{
let name: String
let age: Int
let color: String
}
do {
let decoder = JSONDecoder()
let dog = try decoder.decode(Dog.self, from: dogData)
print(dog)
let cat = try decoder.decode(Cat.self, from: catData)
print(cat)
}
extension Animal {
static func make(fromJSON data: Data) -> Animal? {
let decoder = JSONDecoder()
do {
let dog = try decoder.decode(Dog.self, from: data)
return dog
} catch {
do {
let cat = try decoder.decode(Cat.self, from: data)
return cat
} catch {
return nil
}
}
}
}
if let animal = Dog.make(fromJSON: dogData) {
print(animal)
}
if let animal2 = Dog.make(fromJSON: catData) {
print(animal2)
}
However you will notice that there are some changes that do have a reason. As a matter of fact you cannot implement the Decodable method init(from: Decoder) throws since it is supposed to chain to the init method which ... does not really work out for a protocol. I chose instead to implement your favourite dispatcher in the Animal.make method, but this ended up as a half baked solution as well. Since protocols are metatypes (probably for a good reason as well) you are not able to call their static methods on the metatype and have to use a concrete one. As the line Dog.make(fromJSON: catData) shows this looks weird to say the least. It would be better to bake this into a top level function such as
func parseAnimal(from data:Data) {
...
}
but still this looks unsatisfactory in another way since it pollutes the global namespace. Probably still the best we can do with the means available.
Given the ugliness of the dispatcher it seems like a bad idea to have JSON with no direct indication of the type since it makes parsing really hard. However, I do not see a nice way to communicate a subtype in JSON in a way that really makes it easy to parse. Have not done any research on this, but it might be your next try.
A better approach would be to use a class instead of a protocol and use classes instead of structs. Your Dog and Cat classes will be subclasses of Animal
class Animal: Codable {
let name: String
let age: Int
private enum CodingKeys: String, CodingKey {
case name
case age
}
}
class Dog: Animal {
let type: String
private enum CodingKeys: String, CodingKey {
case type
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
try super.init(from: decoder)
}
}
class Cat: Animal {
let color: String
private enum CodingKeys: String, CodingKey {
case color
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.color = try container.decode(String.self, forKey: .color)
try super.init(from: decoder)
}
}
let data = Data(contentsOf: url)
let animal = JSONDecoder().decode(Animal.self, from: data)
Related
I am trying to decode an object like MyPets with the following object below. I tried using 4 CodingKeys cat, dog, catName and dogName but since the names are not in the MyPets struct, they aren't available to decode and then join with the others to fill out the Dog and Cat structs. I also tried a nestedContainer for the name properties with no luck. Any ideas? Thank you!
struct MyPets: Decodable {
let dog: Dog
let cat: Cat
// enum CodingKeys: String, CodingKey {
// case dog
// case cat
// case catName
// case dogName
// }
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
// values only has dog and cat
}
}
struct Dog {
let name: String
let weight: Int
let food: String
}
struct Cat {
let name: String
let weight: Int
let food: String
}
With the following object:
{
"catName": "fluffy",
"dogName": "softy",
"cat": {
"food": "fancyFeast",
"weight": "8"
},
"dog": {
"food": "kibble",
"weight": "9"
}
}
I want to do something like this to recreate the Cat struct
let catName = (try values.decode(String.self, forKey: .catName))
self.cat = Cat(name: catName, etc...)
This can be used by having two different CodingKey enums, one for the actual properties and one that we decode to local variables.
struct MyPets: Decodable {
var dog: Dog
var cat: Cat
enum CodingKeys: String, CodingKey {
case dog
case cat
}
enum HiddenKeys: String, CodingKey {
case dogName
case catName
}
Then we make use of them in init(from:) by creating two different containers
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dog = try container.decode(Dog.self, forKey: .dog)
cat = try container.decode(Cat.self, forKey: .cat)
let otherContainer = try decoder.container(keyedBy: HiddenKeys.self)
let dogName = try otherContainer.decode(String.self, forKey: .dogName)
let catName = try otherContainer.decode(String.self, forKey: .catName)
self.dog.name = dogName
self.cat.name = catName
}
Dog and Cat (not shown here) needs to conform to Decodable as well and use their own CodingKey enum
struct Dog: Decodable {
var name: String = ""
let weight: Int
let food: String
enum CodingKeys: String, CodingKey {
case weight, food
}
}
I have the following Codable protocol containing a variable which I would like to exclude from the codable ones.
Problem is that I can't use the CodingKeys enum made for that within my own protocol: Type 'CodingKeys' cannot be nested in protocol 'Animal'.
protocol Animal: Codable {
var name: String { get set }
var color: String { get }
var selfiePicture: Selfie { get }
// Not possible
enum CodingKeys: String, CodingKey {
case name
case color
}
}
How could I resolve this?
EDIT with more code and more specific example
Animal is used by several structs (can't be classes):
struct Frog: Animal {
var name: String
var color: String
// extra variables on top of Animal's ones
var isPoisonous: Bool
var selfiePicture = [...]
}
It is also used as a variable array on another top-codable object:
final class Farm: Codable {
var address: String
// more variables
var animals: [Animal]
enum CodingKeys: String, CodingKey {
case address
case animals
}
convenience init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
address = try values.decode(String.self, forKey: .address)
animals = try values.decode([Animal].self, forKey: .animals) // ERROR --> Protocol 'Animal' as a type cannot conform to 'Decodable'
}
}
One way to solve this is to use composition, move the common properties to a new type and use that type in the protocol.
So let's make a type for the common properties and let that type hold the CodingKey enum
struct AnimalCommon: Codable {
var name: String
var color: String
var selfiePicture: Selfie = Selfie()
enum CodingKeys: String, CodingKey {
case name
case color
}
}
And the protocol becomes
protocol Animal: Codable {
var common: AnimalCommon { get set }
}
After that it will be quite easy to implement the actual Animal types, for example
struct Frog: Animal {
var common: AnimalCommon
var isPoisonous: Bool
}
let frog = Frog(common: AnimalCommon(name: "kermit", color: "green"), isPoisonous: false)
do {
let data = try JSONEncoder().encode(frog)
if let string = String(data: data, encoding: .utf8) { print(frog) }
} catch {
print(error)
}
You can also add an extension to the protocol with computed properties so you can access the properties directly, i.e frog.name = "Kermit"
extension Animal {
var name: String {
get {
common.name
}
set {
common.name = newValue
}
}
var color: String {
common.color
}
}
Following Protocol type cannot conform to protocol because only concrete types can conform to protocols, I had to give up on the protocol and use a struct + enum inside instead.
Even though #JoakimDanielson's answer was promising, it does not fix an error I have while trying to decode the Animal array from my Farm class: Protocol 'Animal' as a type cannot conform to 'Decodable'.
Here is how my model looks like at the end:
struct Animal: Codable {
enum Species: Codable {
case frog(FrogSpecificities)
case ...
var selfiePicture: Selfie {
switch self {
case .frog(let frogSpecificities):
return frogSpecificities.selfie
case ...
...
}
}
enum CodingKeys: String, CodingKey {
case FrogSpecificities
case ...
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let frogSpecificities = try? values.decode(FrogSpecificities.self, forKey: .frogSpecificities) {
self = .frog(frogSpecificities)
} else if ...
...
} else {
// throw an error here if no case was decodable
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .frog(let frogSpecificities):
try container.encode(frogSpecificities, forKey: .frogSpecificities)
case ...:
...
}
}
}
var name: String
let color: String
var species: Species
enum CodingKeys: String, CodingKey {
case name
case color
case specificities
}
}
struct FrogSpecificities: Codable {
let isPoisonous: Bool
let selfie = Selfie()
enum CodingKeys: String, CodingKey {
case isPoisonous
}
}
final class Farm: Codable {
var address: String
var animals: [Animal]
enum CodingKeys: String, CodingKey {
case address
case animals
}
convenience init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
address = try values.decode(String.self, forKey: .address)
animals = try values.decode([Animal].self, forKey: .animals) // Works fine
}
}
My Farm object can now contains an Animal arrays with specific codable struct for each one of them. It can also contains variables which are not codable.
I can access each specificities of my animals like this:
if let firstAnimel = MyFarm.animals.firstObject,
case .frog(let frogSpecificities) = firstAnimal.species {
print(frogSpecificities.isPoisonous)
}
I have a service whose response vary based on the orderNumer(input parameter which I pass to service). all most all object are same but only one object(ex meta object which is dictionary)vary based on order number. But I would like to reuse the same model class everywhere but due to different meta object can't be able to create that meta object in model class.I can achieve it by creating individual model class but not a right solution.
struct BookingInfo: Codable {
let created_time: Int
// Some other 20 key value pairs
let oms_meta: Meta /* oms_meta": {
"package": {
"description": "gzhdjjdjd"
}*/
}
// Meta for Order1
struct Meta: Codable {
let package: Description
enum CodingKeys : String, CodingKey {
case package = "package"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
package = try values.decode(Description.self, forKey: .package)
}
}
// Meta for Order 2
struct Meta: Codable {
let customer_payment_details: Int
let items: Int // this can be anything dictinary or some time array of dictionary
enum CodingKeys : String, CodingKey {
case package = "customer_payment_details"
case items = "items"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
customer_payment_details = try values.decode(Int.self, forKey: .package)
items = try values.decode(Int.self, forKey: .package)
}
}
Only meta parameters are varying from service to service.
Thanks in advance
If you want to only use 1 model, you should provide a variable for all the possible keys in the dictionary and set them as optional.
Let's say all orders have an orderNumber, but only some have an orderImage you could do it like this:
struct Order: Codable {
var orderNumber: Int
var orderImage: Data?
var orderName: String?
var orderDescription: String?
}
This way, the returned data only has to contain an orderNumber to be able to decode it.
Edit: You'd have to make a seperate model for each type of 'meta data' as they seem to be very different from eachother. Note that there is nothing wrong with creating a lot of model classes as long as they represent a specific data object.
[
{
"created_time": 1,
"oms_meta": {
"name": "1"
}
},
{
"created_time": 1,
"oms_meta": [
1
]
}
]
Suppose we have a response something like this, where you can have multiple oms_meta objects. So what we can do here is to specify an enum which will contain both array as well as dictionary, it doesn't matter if the service sends you array or dictionary if you need to specify other data types mentioned it in that enum. So it will be like,
struct BookingInfoElement: Codable {
let createdTime: Int?
let omsMeta: OmsMetaUnion?
enum CodingKeys: String, CodingKey {
case createdTime = "created_time"
case omsMeta = "oms_meta"
}
}
enum OmsMetaUnion: Codable {
case integerArray([Int])
case omsMetaClass(OmsMetaClass)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([Int].self) {
self = .integerArray(x)
return
}
if let x = try? container.decode(OmsMetaClass.self) {
self = .omsMetaClass(x)
return
}
throw DecodingError.typeMismatch(OmsMetaUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for OmsMetaUnion"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .integerArray(let x):
try container.encode(x)
case .omsMetaClass(let x):
try container.encode(x)
}
}
}
struct OmsMetaClass: Codable {
let name: String?
enum CodingKeys: String, CodingKey {
case name = "name"
}
}
typealias BookingInfo = [BookingInfoElement]
So, if you specifically want to have multiple types for a single variable you can create another enum with that multiple types and assign accordingly. You can access that variable using switch like, and check on array and dictionary types.
After doing some analyses I found the solution for my requirement. As I wanted to reuse the Model class and OMS_Meta dictionary different from product to product, I'm adding omsMeta to JSONDecoder().userInfo property
struct BookingInfo: Codable {
let created_time: Int
let oms_meta: [String: Any]
}
public static let metaUserInfo = CodingUserInfoKey(rawValue: "oms_meta")!
decoder.userInfo[BookingInfo.metaUserInfo] = jsonDecode
By doing this I can re use my model class.
It's not about decoding a property value with multiple types (int, string)..
I have an object called data it can return multiple types, What could be done at this point may look something like this :
enum MyData: Codable {
case ObjOne(groupObject)
case ObjTwo(imageObject)
init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer()
if let v = try? value.decode(groupObject.self) {
self = .ObjOne(v)
return
} else if let v = try? value.decode(imageObject.self) {
self = .ObjTwo(v)
return
}
throw Rating.ParseError.notRecognizedType(value)
}
enum ParseError: Error {
case notRecognizedType(Any)
}
}
The issue here is that i try to make MyData decode the object based on another value that was used in the previous decoding process, in short words, i want to pass a value to MyData so it can determine which to decode
I have this
enum ContentType: String, Codable {
case linear
case grid
case slider
}
And i want MyData to know about this ContentType value so MyData can determine how the flow will go,
So where did ContentType come from ? it's in the same list of properties in the previous main object, coming from something that looks like this
struct Catalog: Codable {
var dataType: ContentType?
var data: MyData?
}
What i want to achieve in more simple words ?
struct Catalog: Codable {
var dataType: ContentType?
var data: MyData<dataType>? <--// i know this is not possible,
// -- but i want MyData to know about the dataType value that will be decoded
}
--------- JSON i want to parse
[{
"data_type": "group",
"data": {
"group_id": 127 // this refers to object : groupObject
}
},
{
"data_type": "image",
"data": {
"image": "http://google.com/favicon" // this is referring : imageObject
}
}
]
You see the point above, is that "data" can return different objects, based on the value of data_type
Rather than using generics I created an empty protocol that conforms to Decodable and used that as the type for data. Then the content structs needs to conform to this protocol.
protocol MyData: Decodable {}
struct Group: MyData {
let groupId: Int
}
struct Image: MyData {
let image: String
}
struct Catalog: Decodable {
var dataType: String
var data: MyData
enum CodingKeys: String, CodingKey {
case dataType, data
}
enum ParseError: Error {
case notRecognizedType(Any)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dataType = try container.decode(String.self, forKey: .dataType)
switch dataType {
case "group":
data = try container.decode(Group.self, forKey: .data)
case "image":
data = try container.decode(Image.self, forKey: .data)
default:
throw ParseError.notRecognizedType(dataType)
}
}
}
Note that I didn't use the enum ContentType in the init because it didn't match the sample json data but that should be easily fixed.
Standard code for using this solution
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode([Catalog].self, from: data)
print(result)
} catch {
print(error)
}
Our json-over-rest(ish) API follows a pattern of encoding URLs accessible from a particular object in a list format, under the #links key. Made-up example:
{ "id": "whatever",
"height_at_birth": 38,
"#links": [
{ "name": "shield-activation-level",
"url": "https://example.com/some/other/path" },
{ "name": "register-genre-preference",
"url": "https://example.com/some/path" }
]
}
On the Swift side we use phantom types and optionality for type-safety. For instance the above json might correspond to a struct like:
struct Baby {
let id: String
let heightAtBirth: Int
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
}
The phantom types ensure that we can't post a feeding schedule to the registerGenrePreference URL by accident, and the optionality indicates that a well-formed Baby-json will always contain an #links entry for registerGenrePreference but that the other two links might or might not be present. So far so good.
I would like to use Decodable to consume this json format, ideally with a minimum of init(decoder:Decoder) custom implementations. But I am stumped by the #links entries.
I think I see what this would look like if I do the entire decoding by hand:
get the baby's container,
from it get a nested unkeyed container for the #links key
iterate over its values (which should be [String:String] dicts) and build a dict matching names to URLs
for each link Baby expects, look it up in the dict (and throw if the property was non-optional and the link is missing)
But steps 2 and 3 are the same for every class following this pattern (not ideal) and even worse, having to do this also prevents me from using the compiler-provided Decodable implementation so I also have to manually decode all the other properties of Baby.
If it helps I'm perfectly happy restructuring Baby; one obvious step that might help would be:
struct Baby {
let id: String
let heightAtBirth: Int
let links: Links
struct Links {
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
}
}
And of course I expect we'll have to add coding keys, even if only for the snake/camel-case conversion and the #:
enum CodingKeys: String, CodingKey {
case id
case heightAtBirth = "height_at_birth"
case links = "#links"
}
I could probably make a manual Decodable conformance for Baby.Links, following the pattern above, but that still will mean repeating the "get the unkeyed collection, transform it to a dict, look up coding-keys in the dict" steps once for each class following this pattern.
Is there a way to centralise this logic?
You actually have a well defined structure for your links. They are a dictionary [String : String] so you can use that to your advantage in using Decodable.
You may want to consider setting up your structs like below. The links are decoded from the JSON, and an extension provides your optionality of the exact links you are looking for.
A Linkable protocol could be used to add the conformance to any class/struct that needs it.
import Foundation
struct Link: Decodable {
let name: String
let url: String
}
protocol Linkable {
var links: [Link] { get }
}
extension Linkable {
func url(forName name: String) -> URL? {
guard let path = links.first(where: { $0.name == name })?.url else {
return nil
}
return URL(string: path)
}
}
struct Baby: Decodable, Linkable {
let id: String
let heightAtBirth: Int
let links: [Link]
enum CodingKeys: String, CodingKey {
case id = "id"
case heightAtBirth = "height_at_birth"
case links = "#links"
}
static func makeBaby(json: String) throws -> Baby {
guard let data = json.data(using: .utf8) else {
throw CocoaError.error(.fileReadUnknown)
}
return try JSONDecoder().decode(Baby.self, from: data)
}
}
extension Baby {
var registerGenrePreference: URL? {
return url(forName: "register-genre-preference")
}
var shieldActivationLevel: URL? {
return url(forName: "shield-activation-level")
}
}
let baby = try Baby.makeBaby(json: json)
baby.registerGenrePreference
baby.shieldActivationLevel
The following pattern gives me complete type-safety, and most of the implementation lives in the one-off utility class UntypedLinks. Each model class needs to define a nested class Links with a custom decode(from: Decoder), but the implementation of those is completely boilerplate (probably can be automated with simple code-generation tooling) and reasonably readable.
public struct Baby: Decodable {
public let id: String
public let heightAtBirth: Int
public let links: Links
enum CodingKeys: String, CodingKey {
case id
case heightAtBirth = "height_at_birth"
case links = "#links"
}
public struct Links: Decodable {
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
enum CodingKeys: String, CodingKey {
case registerGenrePreference = "register-genre-preference"
case shieldActivationLevel = "shield-activation-level"
case magicPowers = "magic-powers"
}
public init(from decoder: Decoder) throws {
let links = try UntypedLinks<CodingKeys>(from: decoder)
registerGenrePreference = try links.required(.registerGenrePreference)
shieldActivationLevel = links.optional(.shieldActivationLevel)
magicPowers = links.optional(.magicPowers)
}
}
}
public class UntypedLinks<CodingKeys> where CodingKeys: CodingKey {
let links: [String: String]
let codingPath: [CodingKey]
class UntypedLink: Codable {
let name: String
let url: String
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var links: [String: String] = [:]
while !container.isAtEnd {
let link = try container.decode(UntypedLink.self)
links[link.name] = link.url
}
self.links = links
self.codingPath = container.codingPath
}
func optional<Phantom>(_ name: CodingKeys) -> Link<Phantom>? {
return links[name.stringValue].map(Link.init)
}
func required<Phantom>(_ name: CodingKeys) throws -> Link<Phantom> {
guard let link: Link<Phantom> = optional(name) else {
throw DecodingError.keyNotFound(
name,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Link not found")
)
}
return link
}
}