Home > Enterprise >  Decoding objects without knowing their type first
Decoding objects without knowing their type first

Time:08-17

There is a likelihood this is an XY problem, I am open to these suggestions as well !

I am trying to work with Minecraft save data. Minecraft encodes Entities (basically anything that is not strictly a block) with their type inside an id property . The file then contains a big array of entities, which I want to decode and instantiate.

The problem is that, using Decodable, I must know an object's type before I start instantiating it like container.decode(Zombie.self). I can't figure out how to create a function that would read the id and return the right type of entity ?

I think this explains what I need better than any explanation could :

//Entity objects don't actually store their ID since re-encoding it is trivial.
protocol Entity : Decodable {var someProperty : Int {get set}}
struct Zombie : Entity {var someProperty : Int}
struct Skeleton : Entity {var someProperty : Int}

//Using JSON instead of SNBT so we can use JSONDecoder
let jsonData = """
[
    {
        "id":"zombie",
        "someProperty":"3"
    },
    {
        "id" : "skeleton",
        "someProperty":"3"
    }
]
"""

struct EntityList : Decodable {
    var list : [any Entity] = []
    init(from decoder : Decoder) throws {
        var container = try decoder.unkeyedContainer()
        //What should we put here ?
    }
}

let decoder = JSONDecoder()
let entityList = try decoder.decode(EntityList.self, from: Data(jsonData.utf8))
//entityList should be [Zombie, Skeleton]

At the moment I'm looking into the Factory pattern, maybe that's an interesting lead ? In any case, thank you for your help !


( Please note this question has nothing to do with decoding the actual binary contents of the file, it was honestly quite hard to do but I already have a working Encoder / Decoder. It is only about unpacking those contents, hence why I just used JSON in the example above, since we have a common Decoder for that. )

CodePudding user response:

I honestly haven't used the new any syntax enough to know if that can help but I have done what you're trying to do numerous times and here is how I do it.

Set up the data first

We first declare what a Zombie and a Skeleton are. They could just inherit from a protocol or they could be separate structs...

struct Zombie: Decodable {
  let someProperty: Int
}

struct Skeleton: Decodable {
  let someProperty: Int
  let skeletonSpecificProperty: String
}

Then we can turn your array of [anyEntityType] into a homogeneous array by using an enum and embedding the entities into it...

enum Entity: Decodable {
  case zombie(Zombie)
  case skeleton(Skeleton)
}

Decode the enum given your JSON structure

We have to provide a custom decoder for the Entity type...

init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: RootKeys.self)

  // First get the `id` value from the JSON object
  let type = try container.decode(String.self, forKey: .id)

  // check the value for each type of entity we can decode
  switch type {

  // for each value of `id` create the related type
  case "zombie":
    let zombie = try Zombie(from: decoder)
    self = .zombie(zombie)
  case "skeleton":
    let skeleton = try Skeleton(from: decoder)
    self = .skeleton(skeleton)
  default:
    // throw an error here... unsupported type or something
  }
}

This should now let you decode an array of Entities from JSON into an [Entity] array.

Deal with "unknown" types

There is an extra step required for dealing with the "unknown" types. For instance, in the code above. If the JSON contains "id": "creeper" this will error as it can't deal with that. And you'll end up with your whole array failing to decode.

I've created a couple of helper functions that help with that...

If you create an object like...

struct Minecraft: Decodable {
  let entities: [Entity]

  enum RootKeys: String, CodingKey {
    case entities
  }
}

And these helpers...

extension KeyedDecodingContainer {
  func decodeAny<T: Decodable>(_ type: T.Type, forKey key: K) throws -> [T] {
    var items = try nestedUnkeyedContainer(forKey: key)

    var itemsArray: [T] = []
    while !items.isAtEnd {
      guard let item = try? items.decode(T.self) else {
        try items.skip()
        continue
      }
      itemsArray.append(item)
    }
    return itemsArray
  }
}

private struct Empty: Decodable { }

extension UnkeyedDecodingContainer {
  mutating func skip() throws {
    _ = try decode(Empty.self)
  }
}

You can create a custom decoder for the Minecraft type like this...

init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: RootKeys.self)
  self.entities = try container.decodeAny(Entity.self, forKey: .entities)
}
  • Related