Swift decode object with some common elements, and some changing - json

There are a few different JSON objects that could be returned from a single endpoint. There will always be some elements in the object, including a type, and I know all the types. But then the different objects could have some different elements. Here's some example JSON
// Story.json
{
"id": 1,
"time": 1314211127,
"type": "story",
"title": "Awesome story",
"comment_count": 36
}
// Ad.json
{
"id": 2,
"time": 1316142113127,
"type": "ad",
"image_url": "https://someurl.com/",
"tracking_id": 67814
}
// Comment.json
{
"id": 3,
"time": 131448227,
"type": "comment",
"text": "A comment",
"parent_id": 1
}
So you can see that there's comment elements like id, time, and comment. However, based on the type, the additional elements are different. I'm getting confused trying to decode this. So far I have:
enum ItemType: String, Decodable {
case story
case ad
case comment
}
protocol Item: Decodable {
var id: Int { get }
var time: Int? { get }
var type: ItemType { get }
}
struct AnyItem: Item {
let id: Int
let time: Int?
let type: ItemType
private enum CodingKeys: String, CodingKey {
case id, time, type
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
time = try container.decodeIfPresent(Int.self, forKey: .time)
type = try container.decode(ItemType.self, forKey: .type)
// Then I'm assuming switch on the type here
switch type {
// don't know where to go from here...
}
}
}
I guess I need other types conforming to Item such as StoryItem, AdItem, CommentItem. Then, because the endpoint can return all of these items in one response, I can store it in an array of type [Item]. I just can't figure out how to decode all those types in one response.
Any help appreciated.

An easy method would be to use optionals for the optional keys and they'll be ignored while decoding. Since you already have the type, you can know for sure which keys are gonna be present or you can just unwrap them.
struct Item: Codable {
let id: Int
let time: Int
let type: ItemType
let text: String?
let title: String? // and all other optional properties
}

Related

How to Parse Nested part of a JSON based on a condition in Swift

Other suggested solutions handle type of structure they are part of, While I am not able to figure out how to parse nested part of that same structure based on the value within outer structure.
I have a JSON response whose structure change based on one of the values within outer part of JSON.
For example:
{
"reports": [
{
"reportType": "advance",
"reportData": {
"value1": "true"
}
},
{
"reportType": "simple",
"reportData": {
"value3": "false",
"value": "sample"
}
}
]
}
Using Codable with string as Type for 'report' key fails to parse this json.
I want this report value to be either parsed later and store it as it is or atleast parse it based on the reportType value as this have different structure for each value of reportType.
I have written code based on the the suggested solutions.
enum ReportTypes: String {
case simple, advance
}
struct Reports: Codable {
let reportArray = [Report]
}
struct Report: Decodable {
let reportType: String
let reportData: ReportTypes
enum CodingKeys: String, CodingKey {
case reportType, reportData
}
init(from decoder: Decoder) {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.reportType = try container.decode(String.self, forKey: .reportType)
switch ReportTypes(rawValue: self.reportType) {
case .simple:
???
case .advance:
???
}
}
}
Please look at the switch cases and i'm not sure what to do. I need a solution similar to do this.
Workaround:
The workaround is that to mode that reportType inside the report {} structure and then follow this question How can i parse an Json array of a list of different object using Codable?
New Structure
{
"reports": [
{
"reportType": "advance",
"reportData": {
"reportType": "advance",
"value1": "true"
}
},
{
"reportType": "simple",
"reportData": {
"reportType": "simple",
"value3": "false",
"value": "sample"
}
}
]
}
So it worked out for me this way.
But if changing the structure is not what you can afford then this will not work.
Other possible solution I see and later Question: How to Access value of Codable Parent struct in a nested Codable struct is storing reportType in variable currentReportType from init(from decoder: Decoder) and then write another decoder for struct reportData that will handle decoding based on the value stored in var currentReportType. Write it by following the first link shared.
A reasonable way to decode this JSON is an enum with associated values because the type is known.
Create two structs for the objects representing the data in reportData
struct AdvancedReport: Decodable {
let value1: String
}
struct SimpleReport: Decodable {
let value3, value: String
}
and adopt Decodable in ReportType
enum ReportType: String, Decodable {
case simple, advance
}
The two main structs are the struct Response which is the root object (I renamed it because there are too many occurrences of the term Report) and the mentioned enum with associated values. The key reportType in the child dictionary is not being decoded because it's redundant.
struct Response: Decodable {
let reports : [Report]
}
enum Report: Decodable {
case simple(SimpleReport)
case advance(AdvancedReport)
private enum CodingKeys: String, CodingKey {
case reportType, reportData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let reportType = try container.decode(ReportType.self, forKey: .reportType)
switch reportType {
case .simple: self = .simple(try container.decode(SimpleReport.self, forKey: .reportData))
case .advance: self = .advance(try container.decode(AdvancedReport.self, forKey: .reportData))
}
}
}
After decoding the JSON you can switch on reports
for report in response.reports {
switch report {
case .simple(let simpleReport): print(simpleReport)
case .advance(let advancedReport): print(advancedReport)
}
}

Swift & Codable: how to "bypass" some JSON levels?

I would like to use this Pokémon API to fetch some data and convert it into a Swift Pokemon struct.
Here is an extract of the response I get when fetching Pokemon #142:
{
"id": 142,
"name": "aerodactyl",
"types": [{
"type": {
"name": "rock",
"url": "https://pokeapi.co/api/v2/type/6/"
},
"slot": 1
},
{
"type": {
"name": "flying",
"url": "https://pokeapi.co/api/v2/type/3/"
},
"slot": 2
}
]
}
Here is the struct I wrote to convert this JSON into a Swift type:
struct Pokemon: Codable {
var id: Int
let name: String
var types: [PokemonType]?
}
struct PokemonType: Codable {
var type: PokemonTypeContent
}
struct PokemonTypeContent: Codable {
var name: PokemonTypeNameContent
}
enum PokemonTypeNameContent: String, Codable {
case flying = "flying"
case rock = "rock"
// ...
}
Now here is my problem: when I want to get the Pokemon types, I need to dig into this:
pokemon.types.first?.type.name
I would like to know if I have instead a way of getting the PokemonTypeNameContent array in the Pokemon struct, to do something like this:
struct Pokemon {
var types: [PokemonTypeNameContent]?
}
(I am not interested in getting the slot values).
Thank you for your help!
You can do custom encoding for PokemonTypeNameContent, and traverse through the levels of JSON using nestedContainer
enum PokemonTypeNameContent: String, Decodable {
case flying = "flying"
case rock = "rock"
// ...
enum OuterCodingKeys: CodingKey { case type }
enum InnerCodingKeys: CodingKey { case name }
init(from decoder: Decoder) throws {
// this is the container for each JSON object in the "types" array
let container = try decoder.container(keyedBy: OuterCodingKeys.self)
// this finds the nested container (i.e. JSON object) associated with the key "type"
let innerContainer = try container.nestedContainer(keyedBy: InnerCodingKeys.self, forKey: .type)
// now we can decode "name" as a string
let name = try innerContainer.decode(String.self, forKey: .name)
if let pokemonType = Self.init(rawValue: name) {
self = pokemonType
} else {
throw DecodingError.typeMismatch(
PokemonTypeNameContent.self,
.init(codingPath: innerContainer.codingPath + [InnerCodingKeys.name],
debugDescription: "Unknown pokemon type '\(name)'",
underlyingError: nil
)
)
}
}
}
// Pokemon can then be declared like this:
struct Pokemon: Decodable {
let id: Int
let name: String
let types: [PokemonTypeNameContent]
}
Do note that this means that you lose the option of decoding PokemonTypeNameContent as a regular enum. If you do want to do that, put the custom decoding code into a property wrapper. Note that we would be decoding the entire JSON array, instead of each JSON object.
#propertyWrapper
struct DecodePokemonTypes: Decodable {
var wrappedValue: [PokemonTypeNameContent]
init(wrappedValue: [PokemonTypeNameContent]) {
self.wrappedValue = wrappedValue
}
enum OuterCodingKeys: CodingKey { case type }
enum InnerCodingKeys: CodingKey { case name }
init(from decoder: Decoder) throws {
// container for the "types" JSON array
var unkeyedContainer = try decoder.unkeyedContainer()
wrappedValue = []
// while we are not at the end of the JSON array
while !unkeyedContainer.isAtEnd {
// similar to the first code snippet
let container = try unkeyedContainer.nestedContainer(keyedBy: OuterCodingKeys.self)
let innerContainer = try container.nestedContainer(keyedBy: InnerCodingKeys.self, forKey: .type)
let name = try innerContainer.decode(String.self, forKey: .name)
if let pokemonType = PokemonTypeNameContent(rawValue: name) {
wrappedValue.append(pokemonType)
} else {
throw DecodingError.typeMismatch(
PokemonTypeNameContent.self,
.init(codingPath: innerContainer.codingPath + [InnerCodingKeys.name],
debugDescription: "Unknown pokemon type '\(name)'",
underlyingError: nil
)
)
}
}
}
}
// You would write this in Pokemon
#DecodePokemonTypes
var types: [PokemonTypeNameContent]

JSON decoder for Swift dealing with changing underlying JSON with Array and Dictionary

I am using a third-party API to get data. It is a rather complex payload but I'm experiencing a problem with one return. For this example I'm over-simplifying the structure. This structure actually has 53 entries, 34 of which are structures themselves.
struct MlsItemData: Codable, Hashable {
let mls_id: String
let photos: [MlsItemPhoto]?
let features: [MlsItemFeature]?
let address: MlsItemAddress
let move_in_date: String?
let stories: Int?
let client_flags: MlsItemClientFlags?
let tax_history: [MlsItemTaxHistory]? <-- our propblem child
let new_construction: Bool?
let primary: Bool?
let prop_common: MlsItemPropertyCommon?
There are a whole load of other data objects in this API's results but I'm focusing on one item with the label tax_history. When there is data to be shared the key contains an Array like below.
{
"tax_history": [
{
"assessment": {
"building": null,
"total": 3900,
"land": null
},
"tax": 683,
"year": "2020"
},
{
"assessment": {
"building": null,
"total": 4093,
"land": null
},
"tax": 698,
"year": 2019
}
]
}
When the API has no data to share I was expecting:
"tax_history": [ ]
or
"tax_history": null
or just not in the payload at all. But instead the API is sending:
"tax_history": { }
I'm having difficulty as to how to deal with this in the decoder. Obviously, the built in decoder returns the "Expected to decode Array but found a dictionary instead", but is there a simple way to write a custom decoder for "just" the tax_history key and how would it be written for either getting an Array or an empty dictionary?
Yes, it is possible to decode this unusual payload using JSONDecoder. One way to do so is to use a custom type to represent either the empty or non-empty scenarios, and implement a custom initializer function and attempt to decode both cases to see which one works:
struct TaxHistoryItem: Decodable {
let year: String
// ...
}
enum TaxHistory: Decodable {
case empty
case items([TaxHistoryItem])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let items = try? container.decode([TaxHistoryItem].self) {
self = .items(items)
} else {
struct EmptyObject: Decodable {}
// Ignore the result. We just want to verify that the empty object exists
// and doesn't throw an error here.
try container.decode(EmptyObject.self)
self = .empty
}
}
}
You could create a specific type that holds this array and then write a custom init(from:) for it.
In the init we try to decode the json as an array and if it fails we simply assign an empty array to the property (nil for an optional property is another possible solution but I prefer an empty collection before nil)
struct TaxHistoryList: Codable {
let history: [TaxHistory]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let list = try? container.decode([TaxHistory].self) {
history = list
} else {
history = []
}
}
}
struct TaxHistory: Codable {
let tax: Int
let year: String
// other stuff
}

Decoding JSON structures with minor differences into just one format

I have three API endpoints that result in the same final structure, however the full JSON structure received from the API is a bit different in each of them.
First JSON result
{
"tracks": [{
"name": "Never Gonna Give You Up"
}]
}
Second JSON result
{
"items": [{
"name": "Never Gonna Give You Up"
}]
}
Third JSON result
{
"items": [{
"track": {
"name": "Never Gonna Give You Up"
}
}]
}
I want all of them to look like this
{
"tracks": [{
"name": "Never Gonna Give You Up"
}]
}
For that I'm using three different structures:
First:
struct TopHitsTrackResponse: Decodable {
var tracks: [Track]
}
Second:
struct FavoritesTrackResponse: Decodable {
var tracks: [Track]
enum CodingKeys: String, CodingKey {
case tracks = "items"
}
}
And the third one is the code below.
What I've tried
I have successfully made the first and second JSON results look exactly equal to the wanted result. However, the third one is a bit more complicated for me. Here's what I've tried without success.
struct NestedTrackResponse: Decodable {
let tracks: [Track]
enum CodingKeys: String, CodingKey {
case tracks = "items"
}
enum TrackKeys: String, CodingKey {
case track
}
init(from decoder: Decoder) throws {
let outerContainer = try decoder.container(keyedBy: CodingKeys.self)
let trackContainer = try outerContainer.nestedContainer(keyedBy: TrackKeys.self,
forKey: .tracks)
self.tracks = try trackContainer.decode([Track].self, forKey: .track)
}
struct Track: Decodable {
var name: String
}
}
I'm calling the API with this function
AF.request(urlRequest)
.validate()
.responseDecodable(of: NestedTrackResponse.self) { response in
// It's always resulting in fatal error
guard let data = response.value else {
fatalError("Error receiving tracks from API.")
}
}
// - `AF` is Alamofire, but I've already tried not using it,
// and the error persists
// - `urlRequest` is just the URL of the API and the API key,
// doesn't really matter for this problem
And getting this error
Expected to decode Dictionary<String, Any> but found an array instead.
You have to write a custom initializer with if let expressions to distinguish the cases.
struct Response : Decodable {
let tracks : [Track]
private enum CodingKeys : String, CodingKey { case items, tracks }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let tracks = try? container.decode([Track].self, forKey: .items) {
self.tracks = tracks
} else if let tracks = try? container.decode([Track].self, forKey: .tracks) {
self.tracks = tracks
} else if let items = try? container.decode([Item].self, forKey: .items) {
self.tracks = items.map(\.track)
} else {
throw DecodingError.dataCorruptedError(forKey: .items, in: container, debugDescription: "Unsupported JSON structure")
}
}
}
struct Item : Decodable {
let track : Track
}
struct Track : Decodable {
let name : String
}
As I said in the other answer, recommending to stick to the JSON structure that you receive from the API, this will prevent many headaches in future, as well as having a well-defined networking layer.
struct Track: Decodable {
var name: String
}
struct TopHitsTrackResponse: Decodable {
var tracks: [Track]
}
struct FavoritesTrackResponse: Decodable {
var items: [Track]
}
// though you should name this by the API name,
// like with the other two
struct NestedTrackResponse: Decodable {
var items: [Item]
struct Item: Decodable {
var track: Track
}
}
Basically the above is your networking layer. Now, comes the business layer, which will try to extract the tracks from each kind of response.
You could implement it with protocols, but functions work as well (if not even better):
func tracks(from response: TopHitsTrackResponse) -> [Track] {
response.tracks
}
func tracks(from response: FavoritesTrackResponse) -> [Track] {
response.items
}
func tracks(from response: NestedTrackResponse) -> [Track] {
response.items.map(\.track)
}
Then, in your API callbacks, you simply call tracks(from:), in order to extract the most wanted results.

(Swift) Parse JSON with JSONDecoder that has an object that changes structure depending on message [duplicate]

This question already has answers here:
Swift 4 Decodable with keys not known until decoding time
(3 answers)
Closed 4 years ago.
Problem basically comes down to this. My app is receiving messages in this JSON format:
{
"action": "ready",
"data": null
}
or
{
"action": "error",
"data": {
"code": String,
"exception": String,
"status": Int
}
}
or
{
"action": "messageRequest",
"data": {
"recipientUserId": String,
"platform": String,
"content": String
}
}
or
{
"action": "tabsChange",
"data": {
"roomsTabs": [{
"configuration": {
"accessToken": STRING,
"url": STRING,
"userId": STRING
},
"id": STRING,
"isOnline": BOOLEAN,
"isUnread": BOOLEAN,
"lastActive": NUMBER,
"name": STRING,
"participantBanned": BOOLEAN,
"platform": STRING,
"secondParticipant": {
"id": STRING,
"platform": STRING,
"userId": STRING
},
"secondParticipantId": STRING,
"state": STRING,
"unreadMessages": NUMBER
]}
}
}
As you can see the data object has different structure depending on a message and it can get large (and there's more than 10 of them).
I don't want to parse everything by hand, field-by-field, ideal solution would of course be:
struct ChatJsCommand: Codable {
let action: String
let data: Any?
}
self.jsonDecoder.decode(ChatJsCommand.self, from: jsonData))
Of course because of Any this can't conform to Codable. I can of course extract manually only the action field, create a map of actions (enum) to struct types and then do JSONDecoder().decode(self.commandMap[ActionKey], data: jsonData). This solution would probably also require some casting to proper struct types to use the objects after parsing.
But maybe someone has a better approach? So the class isn't 300 lines? Any ideas greatly appreciated.
Let's start by defining a protocol for data, it can be an empty protocol. That will help us later:
protocol MessageData: Decodable {
}
Now let's prepare our data objects:
struct ErrorData: MessageData {
let code: String
let exception: String
let status: Int
}
struct MessageRequestData: MessageData {
let recipientUserId: String
let platform: String
let content: String
}
(use optionals where needed)
Now, we also have to know the data types:
enum ActionType: String {
case ready
case error
case messageRequest
}
And now the hard part:
struct Response: Decodable {
let action: ActionType
let data: MessageData?
private enum CodingKeys: String, CodingKey {
case action
case data
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let action = try values.decode(ActionType.self, forKey: .action)
self.action = action
switch action {
case .ready:
data = nil
case .messageRequest:
data = try values.decode(MessageRequestData.self, forKey: .data)
case .error:
data = try values.decode(ErrorData.self, forKey: .data)
}
}
}
The only trick is to decode action first and then parse depending on the value inside. The only problem is that when using Response, you always have to check action first and then cast data to the type you need.
That could be alleviated by merging action and data into one enumeration with associated objects, e.g. :
enum Action {
case ready
case error(ErrorData)
case messageRequest(MessageRequestData)
case unknown
}
struct Response: Decodable {
let action: Action
private enum CodingKeys: String, CodingKey {
case action
case data
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let actionString = try values.decode(String.self, forKey: .action)
switch actionString {
case "ready":
action = .ready
case "messageRequest":
let data = try values.decode(MessageRequestData.self, forKey: .data)
action = .messageRequest(data)
case "error":
let data = try values.decode(ErrorData.self, forKey: .data)
action = .error(data)
default:
action = .unknown
}
}
}