Codable Decode Property with Multiple Object Types Based on Another Value

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



Leave a reply



Submit