I am wondering if there is a feasible way to share common properties across multiple models by using a custom decoder initializer and/or multiple Containers and Codingkeys. Here is the JSON Object i want to map to the corresponding Codable Model:
The property i want to map to Codable is 'sprites'.
As you can see, a lot of properties like back_default, back_female etc.. are sharable across other models like for example sprites->other->dream_world, sprites->other->official-artwork and in sprites->versions->generation-i->red-blue
My goal is to be able to use one single shared model and customize his decoder (maybe by using multiple Containers) thus i can say for example: if the current key to be decode is "official-artwork" decode only a subset of all the CodingKeys exposed by this model object (maybe i need to use another sub-container?) so i will have only 1 decoded field for the key official-artwork, 2 decoded field for the key dream-world and so on.
[EDIT]
At the moment the solution i came up with the other key is this, but some properties are shared and i think there are a better solution (if i understand i to do it properly i can than apply it to the versions key of the model):
struct PokemonSpritesModel: Codable {
//MARK: Properties
var back_default: String?
var back_female: String?
var back_shiny: String?
var back_shiny_female: String?
var front_default: String?
var front_female: String?
var front_shiny: String?
var front_shiny_female: String?
var other: PokemonSpritesModelOther?
var versions: PokemonSpritesModelVersions?
}
struct PokemonSpritesModelOther: Codable {
//MARK: Properties
var dreamWorld: PokemonSpritesModelOtherDreamWorld?
var officialArtwork: PokemonSpritesModelOtherOfficialArtwork?
private enum CodingKeys: String, CodingKey {
case officialArtwork = "official-artwork", dreamWorld = "dream_world"
}
struct PokemonSpritesModelOtherDreamWorld: Codable {
//MARK: Properties
var front_default: String?
var front_female: String?
}
struct PokemonSpritesModelOtherOfficialArtwork: Codable {
//MARK: Properties
var front_default: String?
}
}
I can use the model before and extend it with all the other json object to represent but it will be a lot of repetitive code to write and i think there is a better approach than what i am currently doing. I have read a lot of SO and medium blog post but they didn't helped me.
I am not sure if what am i asking is even possible to do in Swift.
[EDIT 2]
I think it's better to start from scratch to give other people an idea of what i am trying to accomplish. Currently my Codable for that JSON struct is this:
struct PokemonDetailsModel: Decodable {
// MARK: Properties
var name: String?
var base_experience: Int?
var order: Int?
var sprites: PokemonSpritesModel?
var stats: [PokemonStatsModel]?
var types: [PokemonTypesModel]?
}
struct PokemonSpritesModel: Decodable {
//MARK: Properties
// ======== SET Of keys that are repeated
var back_default: String?
var back_female: String?
var back_shiny: String?
var back_shiny_female: String?
var front_default: String?
var front_female: String?
var front_shiny: String?
var front_shiny_female: String?
// ======== SET Of keys that are repeated
var other: PokemonSpritesModelOther?
var versions: PokemonSpritesModelVersions?
}
struct PokemonSpritesModelOther: Decodable {
//MARK: Properties
var dreamWorld: PokemonSpritesModelOtherDreamWorld?
var officialArtwork: PokemonSpritesModelOtherOfficialArtwork?
private enum CodingKeys: String, CodingKey {
case officialArtwork = "official-artwork", dreamWorld = "dream_world"
}
struct PokemonSpritesModelOtherDreamWorld: Decodable {
//MARK: Properties
var front_default: String?
var front_female: String?
}
struct PokemonSpritesModelOtherOfficialArtwork: Decodable {
//MARK: Properties
var front_default: String?
}
}
struct PokemonSpritesModelVersions: Decodable {
//MARK: Properties
var generation_i: PokemonSpritesModelVersionsGenerationsI?
var generation_ii: PokemonSpritesModelVersionsGenerationsII?
// var generation_iii: PokemonSpritesModelVersionsGenerationsIII?
// var generation_iv: PokemonSpritesModelVersionsGenerationsIV?
// var generation_v: PokemonSpritesModelVersionsGenerationsV?
// var generation_vi: PokemonSpritesModelVersionsGenerationsVI?
// var generation_vii: PokemonSpritesModelVersionsGenerationsVII?
// var generation_viii: PokemonSpritesModelVersionsGenerationsVIII?
private enum CodingKeys: String, CodingKey {
case generation_i = "generation-i",
generation_ii = "generation-ii",
// generation_iii = "generation-iii",
// generation_iv = "generation-iv",
// generation_v = "generation-v",
// generation_vi = "generation-vi",
// generation_vii = "generation-vii",
// generation_viii = "generation-viii"
}
struct PokemonSpritesModelVersionsGenerationsI: Decodable {
//MARK: Properties
var red_blue: PokemonSpritesModelVersionsGenerationsColors?
var yellow: PokemonSpritesModelVersionsGenerationsColors?
private enum CodingKeys: String, CodingKey {
case red_blue = "red-blue", yellow
}
}
struct PokemonSpritesModelVersionsGenerationsII: Decodable {
//MARK: Properties
var crystal: PokemonSpritesModelVersionsGenerationsColors?
var gold: PokemonSpritesModelVersionsGenerationsColors?
var silver: PokemonSpritesModelVersionsGenerationsColors?
}
struct PokemonSpritesModelVersionsGenerationsColors: Decodable {
//****** Notes: Some of the keys here (i marked them with an asterisk) are not part of the Set of keys market in the 'sprites' json object that are shared across different models
var back_default: String?
var back_shiny: String?
var back_gray: String? // *
var back_female: String?
var back_shiny_female: String?
var front_default: String?
var front_shiny: String?
var front_gray: String? // *
var front_female: String?
var front_shiny_female: String?
}
I omitted the other generations struct because the concept will be the same.
With this approach everything works but the problem is that i am using always the complete set of keys from the struct 'PokemonSpritesModelVersionsGenerationsColors' (also i should have used the set of keys of 'sprites' but i don't know how to extrapolate that Set and make the sprites struct still working, plus i should also add the new gray color marked with * in 'PokemonSpritesModelVersionsGenerationsColors').
As you said the keys that get repeated several type are a subset of the sprites struct (other and versions json object should be excluded, but as i said earlier the sprites marked with * should be added).
From the research i have made there are 2 possible solutions to accomplish what i want to do:
Create a separate DTO layer (models that represent the data returned by the Server API) and then have another Domain Layer (Models of the App). Each Domain Model will filtrate the Set of keys of the corresponding DTO model so the Domain layers models will match the Server response (the DTO models doesn't match the server response because it will not use only the required keys of the necessary subset)
Use some sort of combination of custom decoder initializer + multiple container through coding key subtype enum (maybe with the help of enum associated value) + maybe other things.
I want to avoid the first approach mainly because of having two layers which will be mostly identical except for the fields and business logic contained in the Domain models to filtrate the necessary keys. I was thinking to proceed with the 2 approach.
I think i have an idea in my mind of how to do it but i can't put all the pieces togheter. Mainly i was reading these blog posts:
- https://matteomanferdini.com/codable/
- https://lostmoa.com/blog/CodableConformanceForSwiftEnumsWithMultipleAssociatedValuesOfDifferentTypes/
- Other Resources
- Use associated value to decode only the subset of keys? (https://forums.swift.org/t/codable-synthesis-for-enums-with-associated-values/41493)
- Multiple containers used as discriminator to decode only a subset of keys? (https://forums.swift.org/t/automatic-codable-conformance-for-enums-with-associated-values-that-themselves-conform-to-codable/11499)
- Checking what type of key the decoder is currently decoding and create only that key? https://stackoverflow.com/a/53270057/2685716)
- Multiple containers? (https://stackoverflow.com/a/57788293/2685716)
The 1 and the 2 blog post i think are the one from which i can take some ideas to build this sprites Json Codable stuct. From the 1 blog post this is the part of the code that maybe is more interesting:
extension Launch: Decodable {
enum CodingKeys: String, CodingKey {
case timeline
case links
case rocket
case flightNumber = "flight_number"
case missionName = "mission_name"
case date = "launch_date_utc"
case succeeded = "launch_success"
case launchSite = "launch_site"
enum RocketKeys: String, CodingKey {
case rocketName = "rocket_name"
}
enum SiteKeys: String, CodingKey {
case siteName = "site_name_long"
}
enum LinksKeys: String, CodingKey {
case patchURL = "mission_patch"
}
}
}
From the 2 blog post this is the code that maybe is what i am looking for that start from a top/root key (our sprites json object?) and it goes down until the bottom of the Json objects that we need to decode.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let key = container.allKeys.first
switch key {
case .empty:
self = .empty
case .editing:
let subview = try container.decode(
EditSubview.self,
forKey: .editing
)
self = .editing(subview: subview)
case .exchangeHistory:
let connection = try container.decode(
Connection?.self,
forKey: .exchangeHistory
)
self = .exchangeHistory(connection: connection)
case .list:
var nestedContainer = try container.nestedUnkeyedContainer(forKey: .list)
let selectedId = try nestedContainer.decode(UUID.self)
let expandedItems = try nestedContainer.decode([Item].self)
self = .list(
selectedId: selectedId,
expandedItems: expandedItems
)
default:
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Unabled to decode enum."
)
)
}
}
So i will try to summarize in the most easy way what i want to do: My idea is to create multiple containers (which some of these will be nested containers and maybe unkeyed containers) of subsets of keys which belongs to a global sets of keys that are shared across different models. Than in the same struct or maybe better in separated struct (maybe i will need to move also the subset of enum keys associated with it inside of it) i will have a custom decoder initializer that will create only the keys defined in this enum subset of keys).
[EDIT 3]
@RobNapier Yes i want to use the same syntax. I still don't know what is the simplest or right approach. Could you provide a complete example with your approach of [String:URL] that enable me for example to decode the following json with the reuse of the shared key?
{
"sprites":
{
"back_default": "some text",
"back_female": "some text",
"back_shiny": "some text",
"back_shiny_female": "some text",
"front_default": "some text",
"front_female": "some text",
"front_shiny": "some text",
"front_shiny_female": "some text",
"other": {
"dream_world": {
"front_default": "some text",
"front_female": "some text"
},
"official-artwork": {
"front_female": "some text"
}
}
}
}