2

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:

JSON Object

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:

  1. 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)

  2. 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:

  1. https://matteomanferdini.com/codable/
  2. https://lostmoa.com/blog/CodableConformanceForSwiftEnumsWithMultipleAssociatedValuesOfDifferentTypes/
  • Other Resources
  1. Use associated value to decode only the subset of keys? (https://forums.swift.org/t/codable-synthesis-for-enums-with-associated-values/41493)
  2. 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)
  3. Checking what type of key the decoder is currently decoding and create only that key? https://stackoverflow.com/a/53270057/2685716)
  4. 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"
        }
    }
  }

}
Coder
  • 83
  • 2
  • 10
  • Please could you share some code what you have tried so far, or give a bit more explanation ? – πter Feb 19 '21 at 19:19
  • please try to check this answer: https://stackoverflow.com/a/66275895/7119329 does it answers your question ? – πter Feb 19 '21 at 19:22
  • I have update my question with the code i am currently using but that i don't like. The SO answer has nothing to do with what i am asking – Coder Feb 19 '21 at 19:58
  • I think I am starting to realize what you want to achieve. I think currently you have achieved what you want. For instance you can decode PokemonSpritesModelOtherDreamWorld as PokemonSpritesModel using the optional properties. However I really recommend you use subclassing or maybe protocol approach here, because having a big object which represents more than just one object with lot's of optional values will cause you some headache in the future – πter Feb 19 '21 at 20:10
  • That model i posted is just a tiny part of the entire model of the 'sprites' i want to map to Codable, if i follow the current approach the code will be very repetitive and of bad quality, but i am sure there is another more elegant way to map the JSON i posted. I want to reuse the PokemonSpritesModel but i don't know how to do it. I have read a lot of medium blog post and SO question but could not understand how to put all the things togheter to map that JSON object. – Coder Feb 19 '21 at 22:09
  • If you have any advice on how to use Protocol + Custom Initializer + Multiple Container you are welcome (I think the solution is about using these things togheter but i didn't get how). – Coder Feb 19 '21 at 22:09
  • 2
    This looks pretty doable, but it's not clear what you want your final API to look like. Do you want to say "decode only the `other/official-artwork` version and return me a Sprite," or do you want to say "decode the whole thing, and I will then query it for `other/official-artwork`?" Do you want "dream_world" and "official-artwork" to be hard-coded types, or are these really dynamic keys that happen to be under `other`? Is the `other` key important here, or would you want to flatten that out? Basically, what final struct would you like this to be if you could have anything? – Rob Napier Feb 20 '21 at 15:58
  • Basically i want to decode the whole json sprites object in the same structure is presented through the api i have mentioned but i want to reuse some of these inside of it to avoid duplicate code. For example currently i access the dream_world sprites with pokemonmodel.sprites.other.dream_world.front_default – Coder Feb 21 '21 at 12:50
  • 1
    Do you *want* to use the syntax `pokemonmodel.sprites.other.dream_world.front_default`? This is very tied to how the data happens to be structured. Do you really want `.other.` in there? Building this on `[String:URL]` dictionaries, rather than so many custom structs makes this much simpler. What's the value of an explicit PokemonSpritesModelOtherOfficialArtwork struct (and dozen more hand-built structs)? In my answer, all of those are just Sprites, and Sprites are allowed to nest. – Rob Napier Feb 21 '21 at 15:36
  • @RobNapier i have updated the question with Edit 3 – Coder Feb 21 '21 at 15:48
  • 1
    My answer is exactly what you're describing, step by step. Look at the example JSON I provide at the top, and the syntax I provide at the bottom for reading it and the `urls` dictionary that maps fields to URLs. I've added a link to a full running version – Rob Napier Feb 21 '21 at 16:22
  • I think this solution is a bit too complicated to mantain, it involves a lot of raw strings. Despite this, i have tried to apply it to the json example i posted but it didn't work. Could you provide please another simpler approach to decode the json example i posted above (better to start from a simple json and then replicate the behavior to the complex one). My json example include also the **other** json Object other than a sequence of [String:URL]. I woulk like to use a Struct approach without having to deal with too much string interpolation as url representation to build the json object. – Coder Feb 21 '21 at 16:46
  • @RobNapier if you can come up with a struct solution that avoid the duplication of keys like i did to apply to the json object i posted that will be all i need. At that point i will replicate your possibile solution to the other struct of the json API. – Coder Feb 21 '21 at 16:50

1 Answers1

1

There are a lot of ways to do this depending on what kind of interface you want this object to have, but here's one approach. When it's done, you'll be able to reference sprites.frontShiny to get the default URL for front_shiny, and you'll be able to get a variant using subscripts like sprites[.official].frontDefault. I expect that you'll want to tailor this implementation a bit to match your usage, but it should get you started.

The structure we're decoding looks like this:

{
  ...
  "sprites": {
    "back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/21.png",
    "back_female": null,
    "back_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/21.png",
    "back_shiny_female": null,
    "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/21.png",
    "front_female": null,
    "front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/21.png",
    "front_shiny_female": null,
    "other": {
      "dream_world": {
        "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/21.svg",
        "front_female": null
      },
      "official-artwork": {
        "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/21.png"
      }
    },
    "versions": {
      "generation-i": {
        "red-blue": {
          "back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/21.png",
          "back_gray": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/gray/21.png",
          "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/21.png",
          "front_gray": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/gray/21.png"
        },
...

The important thing to note is that there is a Sprite structure that gets repeated several times, first at the root level, and then at various nested points under paths like "other/dream_world" and "versions/generation-i/red-blue" (see PokeAPI/sprites).

My assumption is that you don't care much about the "other" and "versions/generation-i" layers, and really just want to talk about specific variants like .dreamWorld and .redBlue. (This can be adapted to exposing about those other layers, but the current implementation intentionally hides them.)

The first tool is the one from Swift 4 decodable with unknown dynamic keys, an arbitrary String key for decoding:

struct AnyStringKey: CodingKey, Hashable, CustomStringConvertible {
    var stringValue: String = ""
    var intValue: Int?
    init?(stringValue: String) { self.stringValue = stringValue }
    init?(intValue: Int) {}
}

The other key tool is going to be a [String: URL] for storage, and variants will be stored like Unix paths: other/official-artwork/....

Starting with the decoding:

struct Sprites {
   var urls: [String: URL] = [:]
}

extension Sprites: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: AnyStringKey.self)

        // Recursively decode, placing any URLs at this layer, and prepending
        // the current key to the path for child layers. Note that this
        // ignores all errors. If the data is malformed, then the dictionary
        // will just be empty.
        for key in container.allKeys {
            if let value = try? container.decode(URL.self, forKey: key) {
                urls[key.stringValue] = value
            } else if let variant = try? container.decode(Sprites.self, forKey: key) {
                for (childKey, value) in variant.sprites {
                    urls["\(key.stringValue)/\(childKey)"] = value
                }
            }
        }
    }
}

With this, you can access the data by key:

// https://pokeapi.co/docs/v2
struct Pokemon: Decodable {
    let sprites: Sprites
}

let sprites = try JSONDecoder().decode(Pokemon.self, from: json).sprites
print(sprites.urls["front_default"]!)

That's ok, but kind of inconvenient to use. We can improve it with some computed properties.

extension Sprites {
    var frontDefault: URL? { urls["front_default"] }
    var frontShiny: URL? { urls["front_shiny"] }
    var frontFemale: URL? { urls["front_female"] }
    var frontShinyFemale: URL? { urls["front_shiny_female"] }
    var backDefault: URL? { urls["back_default"] }
    var backShiny: URL? { urls["back_shiny"] }
    var backFemale: URL? { urls["back_female"] }
    var backShinyFemale: URL? { urls["back_shiny_female"] }
}

Lower levels are also accessible from the urls dictionary:

print(sprites.urls["other/official-artwork/front_default"]!)

But again, this is pretty inconvenient. We can do better with a Variant subscript.

struct Variant: Hashable, CustomStringConvertible {
    let stringValue: String
    init(_ stringValue: String) { self.stringValue = stringValue }
    var description: String { stringValue }

    static var official: Variant { Variant("other/official-artwork/") }
    static var dreamWorld: Variant { Variant("other/dream_world/") }
    static var redBlue: Variant { Variant("versions/generation-i/red-blue/")}
}

And a subscript that rewrites the dictionary, removing the variant prefix from the keys, and removing anything missing that prefix:

extension Sprites {
    subscript(variant: Variant) -> Sprites {
        let prefix = variant.stringValue

        return Sprites(urls: urls.reduce(into: [:]) { (dict, kv) in
            let (key, value) = kv
            if key.hasPrefix(prefix) {
                let newKey = String(key.dropFirst(prefix.count))
                dict[newKey] = value
            }
        })
    }
}

And that allows you to extract new Sprites from an existing Sprites:

let official = sprites[.official]
print(official.frontDefault!)

Full Gist

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you very much for the quick reply, i will take tomorrow the time to read it carefully and give you a proper answer ;) – Coder Feb 20 '21 at 23:04
  • Regarding you answer i am still trying to understand what type of approach you have used, Could you give me a simple example, to follow the overall approach, by creating only the decoding part from the sprites json object down to the keys of the **dream_world** json object?. Also Why you used a function to remove the variant prefixes? – Coder Feb 21 '21 at 12:12
  • I have update my question with more details – Coder Feb 21 '21 at 12:12
  • I just only need to understand how to create the whole decodable struct for example from the sprites json object inside the pokemon model down to one of the other object which have a subset of shared key + maybe other key not part of the subset. If i understand how to do it for this single tree flow i can replicate the same idea to the other branches of the sprites json tree – Coder Feb 21 '21 at 12:57
  • "Why you used a function to remove the variant prefixes?" So that when you access `model.sprites[.official]`, you get back a Sprites that you can access its properties directly (since its URLs are now the top-level). I think you're making too many structs here. The whole sprites tree is just a nested dictionary of tag->URL, and IMO should be treated as that. – Rob Napier Feb 21 '21 at 15:39
  • Yes i agree, in fact i wanted to avoid using a lot of structs by either reduce it with the use of nested containers via multiple enums Codingkey or with your approach of [String:URL] – Coder Feb 21 '21 at 16:01