0

Want to encode an object into a custom structure using JSONEncoder+Encodable.

struct Foo: Encodable {
   var name: String?
   var bars: [Bar]?
}

struct Bar: Encodable {
   var name: String?
   var value: String?
}

let bar1 = Bar(name: "bar1", value: "barvalue1")
let bar2 = Bar(name: "bar2", value: "barvalue2")
let foo = Foo(name: "foovalue", bars: [bar1, bar2])

Default approach of encoding foo gives:

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(foo)
print(String(data: data, encoding: .utf8)!)

Output:

{
   "name": "foovalue",
   "bars": [
      {
         "name": "bar1",
         "value": "barvalue1"
      },
      {
         "name": "bar2",
         "value": "barvalue2"
      }
   ]
}

In the custom output I'd like to use the value of property name as the key, and the values of rest as the value for the mentioned key. The same will be applicable for nested objects. So I'd expect the output to be:

{
    "foovalue": [
       {
          "bar1": "barvalue1"
       },
       {
          "bar2": "barvalue2"
       }
     ]
}

Question is whether Encodable/JSONEncoder supports this. Right now I just process the the first output dictionary and restructure it by iterating the keys.

justintime
  • 352
  • 3
  • 11
  • This is possible by [overriding `Bar`'s `func encode(to:)` method](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) and creating your own `CodingKey`s. However, this format can be somewhat ambiguous — why are you looking to do it this way? What if you need to add further properties in the future? – Itai Ferber Feb 10 '18 at 07:00
  • 1
    you mean `Foo(name: "foo", bars: [bar1, bar2])` – Leo Dabus Feb 10 '18 at 07:06
  • @ItaiFerber That's just the requirement for now :). I was thinking CodingKeys only allows renaming or omitting properties, I'll explore more. – justintime Feb 10 '18 at 07:14
  • @LeoDabus Just edited the question. Thanks! – justintime Feb 10 '18 at 07:14
  • 1
    @justintime If you write your own `encode(to:)` you can define `CodingKey`s however you need to and use them however you like — in this case you can define a struct which conforms to `CodingKey` and can take on any `String` value – Itai Ferber Feb 10 '18 at 07:16
  • @ItaiFerber I think you've pointed me to the right direction, I'll try out dynamic coding keys and my own implementation of `encoder(to:)`. – justintime Feb 10 '18 at 07:31
  • @LeoDabus That's sort of what I'm doing right now, but'd want to see what Encodable is capable of out of the box. :) – justintime Feb 10 '18 at 07:33
  • @LeoDabus For one thing, Codable can support more than one encoding format should you want it. In this small example, yes, the benefit might be marginal, but scaled up, its likely worth it. – Itai Ferber Feb 10 '18 at 07:54
  • @LeoDabus As mentioned above — create a struct which conforms to `CodingKey` that you can assign any string value to. Assign a key the value of `name` and use that key to encode `value` in your overridden `encode(to:)`. I’m not at a computer ATM but I can give an example once I am. – Itai Ferber Feb 10 '18 at 08:02

1 Answers1

2

If you’d like to keep Foo and Bar Encodable, you can achieve this by providing a custom encode(to:) that uses a specific coding key whose value is name:

private struct StringKey: CodingKey {
    let stringValue: String
    var intValue: Int? { return nil }
    init(_ string: String) { stringValue = string }
    init?(stringValue: String) { self.init(stringValue) }
    init?(intValue: Int) { return nil }
}

struct Foo: Encodable {
    var name: String
    var bars: [Bar]

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: StringKey.self)
        try container.encode(bars, forKey: StringKey(name))
    }
}

struct Bar : Encodable {
    var name: String
    var value: String

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: StringKey.self)
        try container.encode(value, forKey: StringKey(name))
    }
}

StringKey can take on any String value, allowing you to encode arbitrarily as needed.

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83