Swift Codable Multiple Types

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

Decoding multiple types for the same key

Object Structure

Try and give the following a go:

// MARK: - Config
struct Config: Codable {
let ids: IDS
let names: Names
}

enum IDS: Codable {
case bool(Bool)
case idArray([ID])

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Bool.self) {
self = .bool(x)
return
}
if let x = try? container.decode([ID].self) {
self = .idArray(x)
return
}
throw DecodingError.typeMismatch(IDS.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for IDS"))
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .bool(let x):
try container.encode(x)
case .idArray(let x):
try container.encode(x)
}
}
}

// MARK: - ID
struct ID: Codable {
let id: String
}

enum Names: Codable {
case bool(Bool)
case nameArray([Name])

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Bool.self) {
self = .bool(x)
return
}
if let x = try? container.decode([Name].self) {
self = .nameArray(x)
return
}
throw DecodingError.typeMismatch(Names.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Names"))
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .bool(let x):
try container.encode(x)
case .nameArray(let x):
try container.encode(x)
}
}
}

// MARK: - Name
struct Name: Codable {
let name: String
}

How I Did It

I generated this using QuickType which can convert JSON into Swift objects. In order to achieve the following above, I entered the JSON as a list of Config options and then just removed the parts I didn't need from the given output...

[
{
"config": {
"ids": false,
"names": [
{
"name": "value1"
},
{
"name": "value2"
}
]
}
},
{
"config": {
"ids": [
{
"id": "id1"
},
{
"id": "id2"
}
],
"names": 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)
}
}
}

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

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 do I handle decoding two possible types for one key in Swift?

If in your JSON the value associated to a key can be sometimes a Float and sometimes a String (besides fixing this error on the backend ) you could follow this approach.

Let's say this is your "funny" JSON

let data = """
[
{
"magicField": "one"
},
{
"magicField":1
}
]
""".data(using: .utf8)!

Good, how can we elegantly represent this kind of data in Swift?

struct Element:Decodable {
let magicField: ???
}

We want magicField to always have a value, sometimes a Float and sometimes a String.

We can solve this with Quantum Mechanics... or with an Enum /h4>

Let's define this type

enum QuantumValue: Decodable {

case float(Float), string(String)

init(from decoder: Decoder) throws {
if let float = try? decoder.singleValueContainer().decode(Float.self) {
self = .float(float)
return
}

if let string = try? decoder.singleValueContainer().decode(String.self) {
self = .string(string)
return
}

throw QuantumError.missingValue
}

enum QuantumError:Error {
case missingValue
}
}

As you can see a value of type QuantumValue can hold a Float or a String. Always 1 and exactly 1 value.

Element

We can now define the general element of our JSON

struct Element:Decodable {
let magicField: QuantumValue
}

Decoding

That's it. Let's finally decode the JSON.

if let elms = try? JSONDecoder().decode([Element].self, from: data) {
print(elms)
}

Result

[
Element(magicField: QuantumValue.string("one")),
Element(magicField: QuantumValue.float(1.0))
]

Update (to answer Rob’s comment)

switch magicField {
case .string(let text):
println(text)
case .float(let num):
println(num)
}

Handling JSON Array Containing Multiple Types - Swift 4 Decodable

I figured out how to decode the mixed included array into two arrays of one type each. Using two Decodable structs is easier to deal with, and more versatile, than having one struct to cover multiple types of data.

This is what my final solution looks like for anyone who's interested:

struct Root: Decodable {
let data: [Post]?
let members: [Member]
let images: [ImageMedium]

init(from decoder: Decoder) throws {

let container = try decoder.container(keyedBy: CodingKeys.self)

data = try container.decode([Post].self, forKey: .data)

var includedArray = try container.nestedUnkeyedContainer(forKey: .included)
var membersArray: [Member] = []
var imagesArray: [ImageMedium] = []

while !includedArray.isAtEnd {

do {
if let member = try? includedArray.decode(Member.self) {
membersArray.append(member)
}
else if let image = try? includedArray.decode(ImageMedium.self) {
imagesArray.append(image)
}
}
}
members = membersArray
images = imagesArray
}

enum CodingKeys: String, CodingKey {
case data
case included
}
}

struct Post: Decodable {
let id: String?
let type: String?
let title: String?
let ownerId: String?
let ownerType: String?

enum CodingKeys: String, CodingKey {
case id
case type
case title
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}

struct Member: Decodable {
let id: String?
let type: String?
let firstName: String?
let lastName: String?

enum CodingKeys: String, CodingKey {
case id
case type
case firstName = "first-name"
case lastName = "last-name"
}
}

struct ImageMedium: Decodable {
let id: String?
let type: String?
let assetUrl: String?
let ownerId: String?
let ownerType: String?

enum CodingKeys: String, CodingKey {
case id
case type
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}

Swift Decodable Request object

You could utilize enum to solve your problem, I have done this in the past with great success.

The code example below can be copy/pasted into the Playground for further tinkering. This is a good way increase your understanding of the inner workings when decoding JSON to Decodable objects.

Hope this can nudge you in a direction that will work in your case.

//: A UIKit based Playground for presenting user interface

import PlaygroundSupport
import Foundation

let json = """
{
"object": "list",
"url": "/v1/refunds",
"has_more": false,
"data": [
{
"id": "some_id",
"object": "refund",
"amount": 100,
},
{
"id": "some_other_id",
"object": "card",
"cardNumber": "1337 1447 1337 1447"
}
]
}
"""

struct Request: Decodable {
let object: String
let url: String
let has_more: Bool
let data: [RequestData] // A list of a type with a dynamic data property to hold object specific information.
}

// Type for objects of type 'card'.
struct Card: Decodable {
let cardNumber: String
}

// Type for objects of type 'refund'.
struct Refund: Decodable {
let amount: Int
}

// A simple enum for every object possible in the 'data' array.
enum RefundObject: String, Decodable {
case card
case refund
}

// An enum with associated values, mirroring the cases from RefundObject.
enum RefundData: Decodable {
case card(Card)
case refund(Refund)
}

// The base data object in the 'data' array.
struct RequestData: Decodable {
let id: String // Common properties can live in the outer part of the data object.
let object: RefundObject
let innerObject: RefundData // An enum that contain any of the cases defined within the RefundData enum.

enum CodingKeys: String, CodingKey {
case id
case object
case data
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.object = try container.decode(RefundObject.self, forKey: .object)

// Here we decode id (I assumed it was a common property), and object.
// The object will be used to determine what the inner part of the data object should be.

switch object {
case .card:
// Set innerObject to the .card case with an associated value of Card.
self.innerObject = .card(
try Card(from: decoder)
)
case .refund:
// Set innerObject to the .refund case with an associated value of Refund.
self.innerObject = .refund(
try Refund(from: decoder)
)
}
}
}

let decoder = JSONDecoder()
let requestData = try decoder.decode(Request.self, from: json.data(using: .utf8)!)

// Example usage of the innerObject:
for data in requestData.data {
switch data.innerObject {
case .card(let card):
print(card.cardNumber)
case .refund(let refund):
print(refund.amount)
}
}

https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html

https://developer.apple.com/documentation/foundation/jsondecoder



Related Topics



Leave a reply



Submit