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
Remove or Edit User Location Blue Pulsing Circle
How to Fix Cocoapod .Modulemap File Not Found
Why Is an Observedobject Array Not Updated in My Swiftui Application
How to Convert Double to Int in Swift
Using a Dispatch_Once Singleton Model in Swift
Get Nth Character of a String in Swift Programming Language
Round Trip Swift Number Types To/From Data
How to Convert Data to Hex String in Swift
Shall We Always Use [Unowned Self] Inside Closure in Swift
Why Create "Implicitly Unwrapped Optionals", Since That Implies You Know There's a Value
Swift - Sort Array of Objects With Multiple Criteria
Overriding Methods in Swift Extensions
How to Make a Swiftui List Scroll Automatically
How to Pop to the Root View Using Swiftui