Codable decode property with multiple object types BASED on another value
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)
}
multiple types in Codable
I definitely agree with @vadian. What you have is an optional rating. IMO this is a perfect scenario for using a propertyWrapper. This would allow you to use this Rated type with any model without having to manually implement a custom encoder/decoder to each model:
@propertyWrapper
struct RatedDouble: Codable {
var wrappedValue: Double?
init(wrappedValue: Double?) {
self.wrappedValue = wrappedValue
}
private struct Rated: Decodable {
let value: Double
}
public init(from decoder: Decoder) throws {
do {
wrappedValue = try decoder.singleValueContainer().decode(Rated.self).value
} catch DecodingError.typeMismatch {
let bool = try decoder.singleValueContainer().decode(Bool.self)
guard !bool else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Corrupted data"))
}
wrappedValue = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
guard let double = wrappedValue else {
try container.encode(false)
return
}
try container.encode(["value": double])
}
}
Usage:
struct AccountState: Codable {
let id: Int?
let favorite: Bool?
let watchlist: Bool?
@RatedDouble var rated: Double?
}
let json1 = #"{"id":550,"favorite":false,"rated":{"value":9.0},"watchlist":false}"#
let json2 = #"{"id":550,"favorite":false,"rated":false,"watchlist":false}"#
do {
let accountState1 = try JSONDecoder().decode(AccountState.self, from: Data(json1.utf8))
print(accountState1.rated ?? "nil") // "9.0\n"
let accountState2 = try JSONDecoder().decode(AccountState.self, from: Data(json2.utf8))
print(accountState2.rated ?? "nil") // "nil\n"
let encoded1 = try JSONEncoder().encode(accountState1)
print(String(data: encoded1, encoding: .utf8) ?? "nil")
let encoded2 = try JSONEncoder().encode(accountState2)
print(String(data: encoded2, encoding: .utf8) ?? "nil")
} catch {
print(error)
}
This would print:
9.0
nil
{"watchlist":false,"id":550,"favorite":false,"rated":{"value":9}}
{"watchlist":false,"id":550,"favorite":false,"rated":false}
Swift structures: handling multiple types for a single property
I ran into the same issue when trying to decode/encode the "edited" field on a Reddit Listing JSON response. I created a struct that represents the dynamic type that could exist for the given key. The key can have either a boolean or an integer.
{ "edited": false }
{ "edited": 123456 }
If you only need to be able to decode, just implement init(from:). If you need to go both ways, you will need to implement encode(to:) function.
struct Edited: Codable {
let isEdited: Bool
let editedTime: Int
// Where we determine what type the value is
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Check for a boolean
do {
isEdited = try container.decode(Bool.self)
editedTime = 0
} catch {
// Check for an integer
editedTime = try container.decode(Int.self)
isEdited = true
}
}
// We need to go back to a dynamic type, so based on the data we have stored, encode to the proper type
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try isEdited ? container.encode(editedTime) : container.encode(false)
}
}
Inside my Codable class, I then use my struct.
struct Listing: Codable {
let edited: Edited
}
Edit: A more specific solution for your scenario
I recommend using the CodingKey protocol and an enum to store all the properties when decoding. When you create something that conforms to Codable the compiler will create a private enum CodingKeys for you. This lets you decide on what to do based on the JSON Object property key.
Just for example, this is the JSON I am decoding:
{"type": "1.234"}
{"type": 1.234}
If you want to cast from a String to a Double because you only want the double value, just decode the string and then create a double from it. (This is what Itai Ferber is doing, you would then have to decode all properties as well using try decoder.decode(type:forKey:))
struct JSONObjectCasted: Codable {
let type: Double?
init(from decoder: Decoder) throws {
// Decode all fields and store them
let container = try decoder.container(keyedBy: CodingKeys.self) // The compiler creates coding keys for each property, so as long as the keys are the same as the property names, we don't need to define our own enum.
// First check for a Double
do {
type = try container.decode(Double.self, forKey: .type)
} catch {
// The check for a String and then cast it, this will throw if decoding fails
if let typeValue = Double(try container.decode(String.self, forKey: .type)) {
type = typeValue
} else {
// You may want to throw here if you don't want to default the value(in the case that it you can't have an optional).
type = nil
}
}
// Perform other decoding for other properties.
}
}
If you need to store the type along with the value, you can use an enum that conforms to Codable instead of the struct. You could then just use a switch statement with the "type" property of JSONObjectCustomEnum and perform actions based upon the case.
struct JSONObjectCustomEnum: Codable {
let type: DynamicJSONProperty
}
// Where I can represent all the types that the JSON property can be.
enum DynamicJSONProperty: Codable {
case double(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Decode the double
do {
let doubleVal = try container.decode(Double.self)
self = .double(doubleVal)
} catch DecodingError.typeMismatch {
// Decode the string
let stringVal = try container.decode(String.self)
self = .string(stringVal)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .double(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
Swift decode JSON array of object based on property value type
You don't need the data
coding key. Just decode the data
property from the same decoder, based on the value of the JSON field:
enum CodingKeys: String, CodingKey {
case dataType = "@type"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dataType = try container.decode(String.self, forKey: .dataType)
switch dataType {
case "FirstObject":
data = try FirstObject(from: decoder)
case "SecondObject":
data = try SecondObject(from: decoder)
default:
throw ParseError.UnknownSchemaType(dataType)
}
}
If you plan to add more types to that list, then the if/else if
can can become hard to manage, you can use a lookup table to address this:
static let typeMapping: [String: MyData.Type] = [ "FirstObject": FirstObject.self ,
"SecondObject": SecondObject.self]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dataType = try container.decode(String.self, forKey: .dataType)
guard let classToDecode = Self.typeMapping[dataType] else {
throw ParseError.UnknownSchemaType(dataType)
}
self.dataType = dataType
self.data = try classToDecode.init(from: decoder)
}
Swift Codable multiple types
You can try
struct Root: Codable {
let description,id: String
let group,groupDescription: String?
let name: String
let value: MyValue
enum CodingKeys: String, CodingKey {
case description = "Description"
case group = "Group"
case groupDescription = "GroupDescription"
case id = "Id"
case name = "Name"
case value = "Value"
}
}
enum MyValue: Codable {
case string(String)
case innerItem(InnerItem)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(InnerItem.self) {
self = .innerItem(x)
return
}
throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
case .innerItem(let x):
try container.encode(x)
}
}
}
struct InnerItem: Codable {
let type, id, name: String
enum CodingKeys: String, CodingKey {
case type = "__type"
case id = "Id"
case name = "Name"
}
}
do {
let result = try JSONDecoder().decode([Root].self,from:data)
print(result)
}
catch {
print(error)
}
Single object might have multiple types - decoding
Declare status
as enum with associated values
enum Status : Decodable {
case success, failure(String)
init(from decoder : Decoder) throws
{
let container = try decoder.singleValueContainer()
do {
try container.decode(String.self)
self = .success
} catch {
let error = try container.decode(StatusError.self)
self = .failure(error.Failure)
}
}
}
and a helper struct
struct StatusError : Decodable {
let Failure : String
}
In Classroom
declare
let status: Status
And check the status
switch classroom.status {
case .success: print("OK")
case .failure(let message): print(message)
}
Of course the error handling can be more robust: Is the success
string really "Success"
? And you can decode the failure
type as [String:String]
and get the value for key Failure
.
How to decode a JSON property with different types?
You will have to implement your own func encode(to encoder: Encoder) throws
and init(from decoder: Decoder) throws
which are both properties of the Codable
protocol. Then change your rating
variable into an enum
Which would look like this:
enum Rating: Codable {
case int(Int)
case string(String)
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let v): try container.encode(v)
case .string(let v): try container.encode(v)
}
}
init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer()
if let v = try? value.decode(Int.self) {
self = .int(v)
return
} else if let v = try? value.decode(String.self) {
self = .string(v)
return
}
throw Rating.ParseError.notRecognizedType(value)
}
enum ParseError: Error {
case notRecognizedType(Any)
}
}
Then on your DetailModel
just change rating: String
to rating: Rating
This works, I have tested with these JSON strings.
// int rating
{
"rating": 200,
"bro": "Success"
}
// string rating
{
"rating": "200",
"bro": "Success"
}
Edit: I've found a better swiftier way of implementing init(from decoder: Decoder) throws
, which produces a better error message, by using this you can now omit the ParseError
enum.
init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer()
do {
self = .int(try value.decode(Int.self))
} catch DecodingError.typeMismatch {
self = .string(try value.decode(String.self))
}
}
Related Topics
Including Zeros in Front of an Integer
Swift: Can't Edit Properties of Custom Table Cell Programmatically
Swift Decodable, Endpoint Returns Completely Different Types
iOS 13 Swiftui: App Crashes Upon Launch on Real Device
Swiftui How to Invoke the Function and Change View from Other Page
Make Swiftui Rectangle Same Height or Width as Another Rectangle
Different Path Url for Filemanager Everytime I Open the App
Swift Uitableviewautomaticdimension Is Not Working
Differencebetween Convenience Init VS Init in Swift, Explicit Examples Better
Equivalent of Skaction Scaletox for a Given Duration in Unity
Aws - Unauthenticated Access Is Not Supported for This Identity Pool in Swift
Swift: Call Self Method Inside Init
Getch() Equivalent in Swift: Read a Single Character from Stdin Without a Newline
Randomizing Node Movement Duration