I have different type of dataset in the given json.
Courses json
{"name" : "University of Florida",
"data": {"GivenCourse" :{"name" : "Introduction to Computer Science", "tuition": 3000}},
"type" : "Courses" }
Professors json
{"name" : "University of Florida",
"data": {"Professor" :{"name" : "Dr.Francis Tudeluv", "age" :53}},
"type" : "Professor" }
I could able to write two structs, one is for Professor and the other one is Courses. However, as you see there are several common elements in the json object except the data. How that could be better handled?
Courses Struct is as follows:
struct Courses: Codable {
let name: String
let data: DataClass
let type: String
}
// MARK: - DataClass
struct DataClass: Codable {
let givenCourse: GivenCourse
enum CodingKeys: String, CodingKey {
case givenCourse = "GivenCourse"
}
}
// MARK: - GivenCourse
struct GivenCourse: Codable {
let name: String
let tuition: Int
}
Professors Class
// MARK: - Welcome
struct Welcome: Codable {
let name: String
let data: DataClass
let type: String
}
// MARK: - DataClass
struct DataClass: Codable {
let professor: Professor
enum CodingKeys: String, CodingKey {
case professor = "Professor"
}
}
// MARK: - Professor
struct Professor: Codable {
let name: String
let age: Int
}
My suggestion is to declare DataClass as enum with associated types
enum DataClass {
case course(Course), professor(Professor)
}
and Type as decodable enum
enum Type : String, Decodable {
case courses = "Courses", professor = "Professor"
}
Then implement init(from decoder and decode the different types depending on the type value. The keys GivenCourse and Professor are ignored.
struct Root: Decodable {
let name: String
let data: DataClass
let type: Type
private enum CodingKeys : String, CodingKey { case name, data, type }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.type = try container.decode(Type.self, forKey: .type)
switch type {
case .courses: let courseData = try container.decode([String:Course].self, forKey: .data)
data = .course(courseData.values.first!)
case .professor: let professorData = try container.decode([String:Professor].self, forKey: .data)
data = .professor(professorData.values.first!)
}
}
}
// MARK: - Professor
struct Professor: Decodable {
let name: String
let age: Int
}
// MARK: - Course
struct Course: Decodable {
let name: String
let tuition: Int
}
let jsonString = """
{"name" : "University of Florida",
"data": {"GivenCourse" :{"name" : "Introduction to Computer Science", "tuition": 3000}},
"type" : "Courses" }
"""
let data = Data(jsonString.utf8)
do {
let result = try JSONDecoder().decode(Root.self, from: data)
print(result)
} catch {
print(error)
}
If the possible types of "data" will remain fairly small, I would just use optionals for each possible type as concrete members of DataClass, with the CodingKeys fully specified, like so:
struct Professor: Codable {
let name: String
let age: Int
}
struct GivenCourse: Codable {
let name: String
let tuition: Int
}
struct DataClass: Codable {
var professor: Professor?
var givenCourse: GivenCourse?
enum CodingKeys: String, CodingKey {
case givenCourse = "GivenCourse"
case professor = "Professor"
}
}
struct SchoolData: Codable {
let name: String
let data: DataClass
}
This will correctly parse the two JSON samples you have, leaving the unused type nil in the DataClass. You could add some computed variables to DataClass if you like for convenient semantics, for example
var isProfessor: Bool {
get {
return nil != self.professor
}
}
This structure also lets you handle the case (if it's possible) of a record having both course and professor data - the DataClass will be able to parse both in one JSON record if it finds it.
Using generic and manual encode/decode:
struct MyDataContainer<T: Codable>: Codable {
let name: String
let data: T
let type: String
}
struct Professor: Codable {
let name: String
let age: Int
}
enum DataKind {
case unknown
case professor(Professor)
case givenCourse(GivenCourse)
}
struct GivenCourse: Codable {
let name: String
let tuition: Int
}
// MARK: - DataClass
struct DataClass: Codable {
let dataKind: DataKind
enum CodingKeys: String, CodingKey {
case professor = "Professor"
case givenCourse = "GivenCourse"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if values.contains(.givenCourse) {
let course = try values.decode(GivenCourse.self, forKey: .givenCourse)
dataKind = .givenCourse(course)
} else if values.contains(.professor) {
let professor = try values.decode(Professor.self, forKey: .professor)
dataKind = .professor(professor)
} else {
dataKind = .unknown
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch dataKind {
case .givenCourse(let course):
try container.encode(course, forKey: .givenCourse)
case .professor(let professor):
try container.encode(professor, forKey: .professor)
case .unknown: break
}
}
}
MyDataContainer<DataClass>
You can simply include both Optional givenCourse and professor models in your DataClass. Each possible model will be decoded thanks to the Codable protocol.
Edit DataClass:
struct DataClass: Codable {
let givenCourse: GivenCourse?
let professor: Professor?
enum CodingKeys: String, CodingKey {
case givenCourse = "GivenCourse"
case professor = "Professor"
}
}
Use/Debug:
let decodedData = try! JSONDecoder().decode(Response.self, from: jsonData)
if decodedData.type == "Professor" {
print(decodedData.data.professor)
} else if decodedData.type == "Courses" {
print(decodedData.data.givenCourse)
}
Related
I'm currently having trouble with utilizing the PokeApi. I have code that allows me to view a pokemon's name and URL to the other JSON for a pokemon, but I'm not quite sure how I would retrieve that data. Here is what I have so far. And here is the link to the api! let pokeList = https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0
import Foundation
// MARK: - Pokemon
struct PokemonList: Codable {
let count: Int
let results: [Result]
}
// MARK: - Result
struct Result: Codable, Identifiable {
let id = UUID()
let name: String
let url: String
enum CodingKeys: String, CodingKey {
case name, url
}
}
// MARK: - Pokemon
struct Pokemon: Codable, Identifiable {
let abilities: [Ability]
let baseExperience: Int
let forms: [Species]
let gameIndices: [GameIndex]
let height: Int
let id: Int
let isDefault: Bool
let locationAreaEncounters: String
let moves: [Move]
let name: String
let order: Int
let species: Species
let sprites: Sprites
let stats: [Stat]
let types: [TypeElement]
let weight: Int
}
// MARK: - Ability
struct Ability: Codable {
let ability: Species
let isHidden: Bool
let slot: Int
}
// MARK: - Species
struct Species: Codable {
let name: String
let url: String
}
// MARK: - GameIndex
struct GameIndex: Codable{
let gameIndex: Int
let version: Species
}
// MARK: - Move
struct Move: Codable {
let move: Species
}
// MARK: - Sprites
struct Sprites: Codable {
let backDefault: String
let backFemale: String
let backShiny: String
let backShinyFemale: String
let frontDefault: String
let frontFemale: String
let frontShiny: String
let frontShinyFemale: String
}
// MARK: - Home
struct Home: Codable {
let frontDefault: String
let frontFemale: String
let frontShiny: String
let frontShinyFemale: String
}
// MARK: - Other
struct Other: Codable {
let home: Home
let officialArtwork: OfficialArtwork
}
// MARK: - OfficialArtwork
struct OfficialArtwork: Codable {
let frontDefault: String
}
// MARK: - Stat
struct Stat: Codable {
let baseStat: Int
let effort: Int
let stat: Species
}
// MARK: - TypeElement
struct TypeElement: Codable {
let slot: Int
let type: Species
}
Code for Webservice to retrieve API Call
import Foundation
class PokeWebService: ObservableObject{
#Published var pokeList: PokemonList?
#Published var pokemonIndvl: Pokemon?
func getPokemonList() async throws {
let (data, _) = try await URLSession.shared.data(from: Constants.url.pokeList)
Task{#MainActor in
self.pokeList = try JSONDecoder().decode(PokemonList.self, from: data)
}
}
func getPokemonFromPokemonList(invlURL: String) async throws{
var plURL: URL = URL(string: invlURL)!
let (data, _) = try await URLSession.shared.data(from: plURL)
Task{#MainActor in
self.pokemonIndvl = try JSONDecoder().decode(Pokemon.self, from: data)
}
}
}
My Content View
import SwiftUI
struct PokeListView: View {
#EnvironmentObject var pokeWebService: PokeWebService
var body: some View {
List(pokeWebService.pokeList?.results ?? []){ pokemon in // <-- here
Text(pokemon.name)
Text(pokemon.url)
}
.task {
do{
try await pokeWebService.getPokemonList()
} catch{
print("---> task error: \(error)")
}
}
}
}
struct PokeListView_Previews: PreviewProvider {
static var previews: some View {
PokeListView()
.environmentObject(PokeWebService())
}
}
Since I'm still fairly new to working with APIs in Swift, how would I retrieve the data from the Pokemon.url?
Thank you!
try something like this approach (note the different model structs). You will need to find from the server docs, which fields are optional and adjust the various structs:
import Foundation
import SwiftUI
struct ContentView: View {
#StateObject var pokeWebService = PokeWebService()
var body: some View {
PokeListView().environmentObject(pokeWebService)
}
}
struct PokeListView: View {
#EnvironmentObject var pokeWebService: PokeWebService
var body: some View {
NavigationStack {
List(pokeWebService.pokeList?.results ?? []){ pokemon in
NavigationLink(pokemon.name, value: pokemon.url)
}
.navigationDestination(for: String.self) { urlString in
PokeDetailsView(urlString: urlString)
}
}
.environmentObject(pokeWebService)
.task {
do {
try await pokeWebService.getPokemonList()
} catch{
print("---> PokeListView error: \(error)")
}
}
}
}
struct PokeDetailsView: View {
#EnvironmentObject var pokeWebService: PokeWebService
#State var urlString: String
var body: some View {
VStack {
Text(pokeWebService.pokemonIndvl?.name ?? "no name")
Text("height: \(pokeWebService.pokemonIndvl?.height ?? 0)")
// ... other info
}
.task {
do {
try await pokeWebService.getPokemon(from: urlString)
} catch{
print("---> PokeDetailsView error: \(error)")
}
}
}
}
class PokeWebService: ObservableObject{
#Published var pokeList: PokemonList?
#Published var pokemonIndvl: Pokemon?
func getPokemonList() async throws {
let (data, _) = try await URLSession.shared.data(from: Constants.url.pokeList)
Task{#MainActor in
self.pokeList = try JSONDecoder().decode(PokemonList.self, from: data)
}
}
func getPokemon(from urlString: String) async throws {
if let url = URL(string: urlString) {
let (data, _) = try await URLSession.shared.data(from: url)
Task{#MainActor in
self.pokemonIndvl = try JSONDecoder().decode(Pokemon.self, from: data)
}
}
}
}
// using https://app.quicktype.io/
// MARK: - PokemonList
struct PokemonList: Codable {
let count: Int
let results: [ListItem] // <-- don't use the word Result
}
// MARK: - ListItem
struct ListItem: Codable, Identifiable {
let id = UUID()
let name: String
let url: String
enum CodingKeys: String, CodingKey {
case name, url
}
}
struct HeldItem: Codable {
let item: Species
let versionDetails: [VersionDetail]
enum CodingKeys: String, CodingKey {
case item
case versionDetails = "version_details"
}
}
struct VersionDetail: Codable {
let rarity: Int
let version: Species
}
// MARK: - Pokemon
struct Pokemon: Codable, Identifiable {
let abilities: [Ability]
let baseExperience: Int
let forms: [Species]
let gameIndices: [GameIndex]
let height: Int
let heldItems: [HeldItem]
let id: Int
let isDefault: Bool
let locationAreaEncounters: String
let moves: [Move]
let name: String
let order: Int
let pastTypes: [String]
let species: Species
let sprites: Sprites
let stats: [Stat]
let types: [TypeElement]
let weight: Int
enum CodingKeys: String, CodingKey {
case abilities
case baseExperience = "base_experience"
case forms
case gameIndices = "game_indices"
case height
case heldItems = "held_items"
case id
case isDefault = "is_default"
case locationAreaEncounters = "location_area_encounters"
case moves, name, order
case pastTypes = "past_types"
case species, sprites, stats, types, weight
}
}
// MARK: - Ability
struct Ability: Codable {
let ability: Species
let isHidden: Bool
let slot: Int
enum CodingKeys: String, CodingKey {
case ability
case isHidden = "is_hidden"
case slot
}
}
// MARK: - Species
struct Species: Codable {
let name: String
let url: String
}
// MARK: - GameIndex
struct GameIndex: Codable {
let gameIndex: Int
let version: Species
enum CodingKeys: String, CodingKey {
case gameIndex = "game_index"
case version
}
}
// MARK: - Move
struct Move: Codable {
let move: Species
let versionGroupDetails: [VersionGroupDetail]
enum CodingKeys: String, CodingKey {
case move
case versionGroupDetails = "version_group_details"
}
}
// MARK: - VersionGroupDetail
struct VersionGroupDetail: Codable {
let levelLearnedAt: Int
let moveLearnMethod, versionGroup: Species
enum CodingKeys: String, CodingKey {
case levelLearnedAt = "level_learned_at"
case moveLearnMethod = "move_learn_method"
case versionGroup = "version_group"
}
}
// MARK: - GenerationV
struct GenerationV: Codable {
let blackWhite: Sprites
enum CodingKeys: String, CodingKey {
case blackWhite = "black-white"
}
}
// MARK: - GenerationIv
struct GenerationIv: Codable {
let diamondPearl, heartgoldSoulsilver, platinum: Sprites
enum CodingKeys: String, CodingKey {
case diamondPearl = "diamond-pearl"
case heartgoldSoulsilver = "heartgold-soulsilver"
case platinum
}
}
// MARK: - Versions
struct Versions: Codable {
let generationI: GenerationI
let generationIi: GenerationIi
let generationIii: GenerationIii
let generationIv: GenerationIv
let generationV: GenerationV
let generationVi: [String: Home]
let generationVii: GenerationVii
let generationViii: GenerationViii
enum CodingKeys: String, CodingKey {
case generationI = "generation-i"
case generationIi = "generation-ii"
case generationIii = "generation-iii"
case generationIv = "generation-iv"
case generationV = "generation-v"
case generationVi = "generation-vi"
case generationVii = "generation-vii"
case generationViii = "generation-viii"
}
}
// MARK: - Sprites
class Sprites: Codable {
let backDefault: String
let backFemale: String?
let backShiny: String
let backShinyFemale: String?
let frontDefault: String
let frontFemale: String?
let frontShiny: String
let frontShinyFemale: String?
let other: Other?
let versions: Versions?
let animated: Sprites?
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backFemale = "back_female"
case backShiny = "back_shiny"
case backShinyFemale = "back_shiny_female"
case frontDefault = "front_default"
case frontFemale = "front_female"
case frontShiny = "front_shiny"
case frontShinyFemale = "front_shiny_female"
case other, versions, animated
}
}
// MARK: - GenerationI
struct GenerationI: Codable {
let redBlue, yellow: RedBlue
enum CodingKeys: String, CodingKey {
case redBlue = "red-blue"
case yellow
}
}
// MARK: - RedBlue
struct RedBlue: Codable {
let backDefault, backGray, backTransparent, frontDefault: String
let frontGray, frontTransparent: String
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backGray = "back_gray"
case backTransparent = "back_transparent"
case frontDefault = "front_default"
case frontGray = "front_gray"
case frontTransparent = "front_transparent"
}
}
// MARK: - GenerationIi
struct GenerationIi: Codable {
let crystal: Crystal
let gold, silver: Gold
}
// MARK: - Crystal
struct Crystal: Codable {
let backDefault, backShiny, backShinyTransparent, backTransparent: String
let frontDefault, frontShiny, frontShinyTransparent, frontTransparent: String
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backShiny = "back_shiny"
case backShinyTransparent = "back_shiny_transparent"
case backTransparent = "back_transparent"
case frontDefault = "front_default"
case frontShiny = "front_shiny"
case frontShinyTransparent = "front_shiny_transparent"
case frontTransparent = "front_transparent"
}
}
// MARK: - Gold
struct Gold: Codable {
let backDefault, backShiny, frontDefault, frontShiny: String
let frontTransparent: String?
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backShiny = "back_shiny"
case frontDefault = "front_default"
case frontShiny = "front_shiny"
case frontTransparent = "front_transparent"
}
}
// MARK: - GenerationIii
struct GenerationIii: Codable {
let emerald: Emerald
let fireredLeafgreen, rubySapphire: Gold
enum CodingKeys: String, CodingKey {
case emerald
case fireredLeafgreen = "firered-leafgreen"
case rubySapphire = "ruby-sapphire"
}
}
// MARK: - Emerald
struct Emerald: Codable {
let frontDefault, frontShiny: String
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontShiny = "front_shiny"
}
}
// MARK: - Home
struct Home: Codable {
let frontDefault: String
let frontFemale: String?
let frontShiny: String
let frontShinyFemale: String?
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontFemale = "front_female"
case frontShiny = "front_shiny"
case frontShinyFemale = "front_shiny_female"
}
}
// MARK: - GenerationVii
struct GenerationVii: Codable {
let icons: DreamWorld
let ultraSunUltraMoon: Home
enum CodingKeys: String, CodingKey {
case icons
case ultraSunUltraMoon = "ultra-sun-ultra-moon"
}
}
// MARK: - DreamWorld
struct DreamWorld: Codable {
let frontDefault: String
let frontFemale: String?
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontFemale = "front_female"
}
}
// MARK: - GenerationViii
struct GenerationViii: Codable {
let icons: DreamWorld
}
// MARK: - Other
struct Other: Codable {
let dreamWorld: DreamWorld
let home: Home
let officialArtwork: OfficialArtwork
enum CodingKeys: String, CodingKey {
case dreamWorld = "dream_world"
case home
case officialArtwork = "official-artwork"
}
}
// MARK: - OfficialArtwork
struct OfficialArtwork: Codable {
let frontDefault: String
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
}
}
// MARK: - Stat
struct Stat: Codable {
let baseStat, effort: Int
let stat: Species
enum CodingKeys: String, CodingKey {
case baseStat = "base_stat"
case effort, stat
}
}
// MARK: - TypeElement
struct TypeElement: Codable {
let slot: Int
let type: Species
}
EDIT-1:
if you have problems with the NavigationStack, use this NavigationView instead.
NavigationView {
List(pokeWebService.pokeList?.results ?? []){ pokemon in
NavigationLink(destination: PokeDetailsView(urlString: pokemon.url)) {
Text(pokemon.name)
}
}
}
I'm having an issue getting codable going. Any help would greatly appreciated. I have the following in my playground
Sample from my JSON file. It has many more elements, reduced it a smaller subset.
{
"metadata" : {
"generated" : {
"timestamp" : 1549331723,
"date" : "2019-02-04 20:55:23"
}
},
"data" : {
"CA" : {
"country-id" : 25000,
"country-iso" : "CA",
"country-eng" : "Canada",
"country-fra" : "Canada",
"date-published" : {
"timestamp" : 1544561785,
"date" : "2018-12-11 15:56:25",
"asp" : "2018-12-11T15:56:25.4141468-05:00"
}
},
"BM" : {
"country-id" : 31000,
"country-iso" : "BM",
"country-eng" : "Bermuda",
"country-fra" : "Bermudes",
"date-published" : {
"timestamp" : 1547226095,
"date" : "2019-01-11 12:01:35",
"asp" : "2019-01-11T12:01:35.4748399-05:00"
}
}
}
}
From The quicktype app. It generated a dictionary for Datum. The way the json is structured, the country abbreviation doesn't have a tag.
import Foundation
// MARK: - Welcome
struct Welcome: Codable {
let metadata: Metadata?
let data: [String: Datum]?
}
// MARK: - Datum
struct Datum: Codable {
let countryID: Int?
let countryISO, countryEng, countryFra: String?
let datePublished: DatePublished?
enum CodingKeys: String, CodingKey {
case countryID = "country-id"
case countryISO = "country-iso"
case countryEng = "country-eng"
case countryFra = "country-fra"
case datePublished = "date-published"
}
}
// MARK: - DatePublished
struct DatePublished: Codable {
var timestamp: Int
var date, asp: String
}
// MARK: - Metadata
struct Metadata: Codable {
var generated: Generated
}
// MARK: - Generated
struct Generated: Codable {
var timestamp: Int
var date: String
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public func hash(into hasher: inout Hasher) {
// No-op
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
From my code, I can load the json file, I'm not sure how to process the data here with the dictionary, and the country not having a name for the country abbreviation.
guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else { return 0 }
let jsonData = try Data(contentsOf: url)
Note: This is a follow up to my earlier question: Swift Codable Parsing keyNotFound
Your data models are already defined correctly (however, I'd suggest some name changes and removing mutability/optionality from the properties).
Once you've parsed the JSON, there's no need to keep the Dictionary, since the keys are actually part of the value under the country-iso key.
So once you decoded your Root object, I would suggest simply keeping root.data.values, which gives you Array<CountryData>, which you can handle easily afterwards.
struct Root: Codable {
let data: [String: CountryData]
}
struct CountryData: Codable {
let countryID: Int
let countryISO, countryEng, countryFra: String
let datePublished: DatePublished
enum CodingKeys: String, CodingKey {
case countryID = "country-id"
case countryISO = "country-iso"
case countryEng = "country-eng"
case countryFra = "country-fra"
case datePublished = "date-published"
}
}
// MARK: - DatePublished
struct DatePublished: Codable {
let timestamp: Int
let date, asp: String
}
do {
let root = try JSONDecoder().decode(Root.self, from: countryJson.data(using: .utf8)!)
let countries = root.data.values
print(countries)
} catch {
error
}
I have a the following structure:
JSON a:
{
"type": "A",
"data": {
"aSpecific": 64
}
}
or JSON b:
{
"type": "B",
"data": {
"bSpecific": "hello"
}
}
Now how does the structure look like to parse any of the above in one go?
enum DataType {
case "A"
case "B"
}
struct Example: Codable {
struct ASpecific: Codable {
var aSpecifiv: Int
}
struct BSpecific: Codable {
var bSpecifiv: String
}
var type: DataType
var data: ??? // Aspecific or BSpecific
}
I want the var data to be specific for the type of the JSON.
How do I do this?
First of all, use String as rawType of enum DataType
enum DataType: String, Decodable {
case A, B
}
Now, you can create init(from:) in the struct Root and add custom parsing as per the JSON.
struct Root: Decodable {
let type: DataType
let aSpecific: Int?
let bSpecific: String?
enum CodingKeys: String, CodingKey {
case type, data, aSpecific, bSpecific
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
type = try container.decode(DataType.self, forKey: .type)
let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data)
aSpecific = try data.decodeIfPresent(Int.self, forKey: .aSpecific)
bSpecific = try data.decodeIfPresent(String.self, forKey: .bSpecific)
}
}
Parsing:
do {
let response = try JSONDecoder().decode(Root.self, from: data)
print(response)
} catch {
print(error)
}
For two different types and one common key my suggestion is an enum with associated values. It's easy to distinguish on the basis of the type and you don't need any optionals.
enum DataType : String, Decodable {
case A, B
}
struct ASpecific: Decodable {
var aSpecific: Int
}
struct BSpecific: Decodable {
var bSpecific: String
}
enum Response : Decodable {
case aType(ASpecific)
case bType(BSpecific)
private enum CodingKeys : String, CodingKey {
case type, data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DataType.self, forKey: .type)
switch type {
case .A: self = .aType(try container.decode(ASpecific.self, forKey: .data))
case .B: self = .bType(try container.decode(BSpecific.self, forKey: .data))
}
}
}
Below is my JSON, and I am not able to decode(using CodingKeys)
The data within the regions key is a Dictionary ("IN-WB", "IN-DL" & so on....), as the keys are dynamic, it can be changed more or less.
Please help me parsing the same using Decodable and Codable.
All the data should be within the single model.
{
"provider_code": "AIIN",
"name": "Jio India",
"regions": [
{
"IN-WB": "West Bengal"
},
{
"IN-DL": "Delhi NCR"
},
{
"IN-TN": "Tamil Nadu"
},
{
"IN": "India"
}
]
}
Just use a Dictionary for the regions.
struct Locations: Codable {
let providerCode: String
let name: String
let regions: [[String: String]]
enum CodingKeys: String, CodingKey {
case providerCode = "provider_code"
case name, regions
}
}
You cannot create a specific model for the regions as you wont know the property names
One of possible approach, without using dictionary. But still we have to found key at first )
I like this style as we can use Regions from beginning.
// example data.
let string = "{\"provider_code\":\"AIIN\",\"name\":\"Jio India\",\"regions\":[{\"IN-WB\":\"West Bengal\"},{\"IN-DL\":\"Delhi NCR\"},{\"IN-TN\":\"Tamil Nadu\"},{\"IN\":\"India\"}]}"
let data = string.data(using: .utf8)!
// little helper
struct DynamicGlobalKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
// model
struct Location: Decodable {
let providerCode: String
let name: String
let regions: [Region]
}
extension Location {
struct Region: Decodable {
let key: String
let name: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicGlobalKey.self)
key = container.allKeys.first!.stringValue
name = try container.decode(String.self, forKey: container.allKeys.first!)
}
}
}
// example of decoding.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let location = try decoder.decode(Location.self, from: data)
Let's say we have a JSON structure like the following (commonly used in Firebase's Realtime Database):
{
"18348b9b-9a49-4e04-ac35-37e38a8db1e2": {
"isActive": false,
"age": 29,
"company": "BALOOBA"
},
"20aca96e-663a-493c-8e9b-cb7b8272f817": {
"isActive": false,
"age": 39,
"company": "QUONATA"
},
"bd0c389b-2736-481a-9cf0-170600d36b6d": {
"isActive": false,
"age": 35,
"company": "EARTHMARK"
}
}
Expected solution:
Using Decodable I'd like to convert it into an array of 3 elements:
struct BoringEntity: Decodable {
let id: String
let isActive: Bool
let age: Int
let company: String
init(from decoder: Decoder) throws {
// ...
}
}
let entities: [BoringEntity] = try! JSONDecoder()...
The id attribute corresponds to the json object's root string , e.g: 18348b9b-9a49-4e04-ac35-37e38a8db1e2.
Workaround:
I have already tried several approaches but couldn't get the id attribute without requiring an auxiliary entity (or using optionals):
/// Incomplete BoringEntity version to make Decodable conformance possible.
struct BoringEntityIncomplete: Decodable {
let isActive: Bool
let age: Int
let company: String
}
// Decode to aux struct
let decoded = try! JSONDecoder().decode([String : BoringEntityIncomplete].self, for: jsonData)
// Map aux entities to BoringEntity
let entities = decoded.map { BoringEntity(...) }
Using init(from: Decoder) isn't as trivial as in other cases since keyedContainer(,) can't be used due to the fact that the key is unknown.
Is Decodable unsuited for these types of cases ?
A couple things before I answer your question:
1: The comment (// id) makes the JSON invalid. JSON does not allow comments.
2: Where does the id property in BoringEntity come from?
struct BoringEntity: Decodable {
let id: String // where is it stored in the JSON???
let isActive: Bool
let age: Int
let company: String
}
If I overlook these things, you can wrap the array of BoringEntity in a struct (BoringEntities). Using [BoringEntity] directly is not advisable since you have to overshadow the default init(from decoder:) of Array.
The trick here is to make JSONDecoder gives you back the list of keys via the container.allKeys property:
struct BoringEntity: Decodable {
let isActive: Bool
let age: Int
let company: String
}
struct BoringEntities: Decodable {
var entities = [BoringEntity]()
// This really is just a stand-in to make the compiler happy.
// It doesn't actually do anything.
private struct PhantomKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PhantomKeys.self)
for key in container.allKeys {
let entity = try container.decode(BoringEntity.self, forKey: key)
entities.append(entity)
}
}
}
Usage:
let jsonData = """
{
"18348b9b-9a49-4e04-ac35-37e38a8db1e2": {
"isActive": false,
"age": 29,
"company": "BALOOBA"
},
"20aca96e-663a-493c-8e9b-cb7b8272f817": {
"isActive": false,
"age": 39,
"company": "QUONATA"
},
"bd0c389b-2736-481a-9cf0-170600d36b6d": {
"isActive": false,
"age": 35,
"company": "EARTHMARK"
}
}
""".data(using: .utf8)!
let entities = try JSONDecoder().decode(BoringEntities.self, from: jsonData).entities
Base entity:
struct BoringEntity: Decodable {
let id: String
let isActive: Bool
let age: Int
let company: String
}
Solution 1: Using an extra struct without the key
/// Incomplete BoringEntity version to make Decodable conformance possible.
private struct BoringEntityBare: Decodable {
let isActive: Bool
let age: Int
let company: String
}
// Decode to aux struct
private let decoded = try! JSONDecoder().decode([String : BoringEntityBare].self, from: jsonData)
// Map aux entities to BoringEntity
let entities = decoded.map { BoringEntity(id: $0.key, isActive: $0.value.isActive, age: $0.value.age, company: $0.value.company) }
print(entities)
Solution 2: Using a wrapper
Thanks to Code Different I was able to combine my approach with his PhantomKeys idea, but there's no way around it: an extra entity must always be used.
struct BoringEntities: Decodable {
var entities = [BoringEntity]()
// This really is just a stand-in to make the compiler happy.
// It doesn't actually do anything.
private struct PhantomKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
}
private enum BareKeys: String, CodingKey {
case isActive, age, company
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PhantomKeys.self)
// There's only one key
for key in container.allKeys {
let aux = try container.nestedContainer(keyedBy: BareKeys.self, forKey: key)
let age = try aux.decode(Int.self, forKey: .age)
let company = try aux.decode(String.self, forKey: .company)
let isActive = try aux.decode(Bool.self, forKey: .isActive)
let entity = BoringEntity(id: key.stringValue, isActive: isActive, age: age, company: company)
entities.append(entity)
}
}
}
let entities = try JSONDecoder().decode(BoringEntities.self, from: jsonData).entities
print(entities)