Support object schemas with only additionalProperties. (#6492)

Previously, we had implemented the Codable protocol by simply claiming conformance, and making sure that each of our internal classes also implemented the Codable protocol. So our model classes looked like:

class MyModel: Codable {
   var propInt: Int
   var propString: String
}

class MyOtherModel: Codable {
   var propModel: MyModel
}

Previously, our additionalProperties implementation would have meant an object schema with an additionalProperties of Int type would have looked like:

class MyModelWithAdditionalProperties: Codable {
   var additionalProperties: [String: Int]
}

But the default implementation of Codable would have serialized MyModelWithAdditionalProperties like this:

{
  "additionalProperties": {
    "myInt1": 1,
    "myInt2": 2,
    "myInt3": 3
  }
}

The default implementation would put the additionalProperties in its own dictionary (which would be incorrect), as opposed to the desired serialization of:

{
  "myInt1": 1,
  "myInt2": 2,
  "myInt3": 3
}

So therefore, the only way to support this was to do our own implementation of the Codable protocol. The Codable protocol is actually two protocols: Encodable and Decodable.

So therefore, this change generates implementations of Encodable and Decodable for each generated model class. So the new generated classes look like:

class MyModel: Codable {
   var propInt: Int
   var propString: String

   // Encodable protocol methods

   public func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: String.self)

        try container.encode(propInt, forKey: "propInt")
        try container.encode(propString, forKey: "propString")
    }

    // Decodable protocol methods

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

        propInt = try container.decode(Int.self, forKey: "propInt")
        propString = try container.decode(String.self, forKey: "propString")
    }

}

class MyOtherModel: Codable {
   var propModel: MyModel

   // Encodable protocol methods

   public func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: String.self)

        try container.encode(propModel, forKey: "propModel")
    }

    // Decodable protocol methods

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

        propModel = try container.decode(MyModel.self, forKey: "propModel")
    }

}
This commit is contained in:
ehyche
2017-09-18 13:04:50 -04:00
committed by wing328
parent 8067612e06
commit b807f6ff96
40 changed files with 814 additions and 247 deletions

View File

@@ -85,6 +85,92 @@ extension UUID: JSONEncodable {
}
}
extension String: CodingKey {
public var stringValue: String {
return self
}
public init?(stringValue: String) {
self.init(stringLiteral: stringValue)
}
public var intValue: Int? {
return nil
}
public init?(intValue: Int) {
return nil
}
}
extension KeyedEncodingContainerProtocol {
public mutating func encodeArray<T>(_ values: [T], forKey key: Self.Key) throws where T : Encodable {
var arrayContainer = nestedUnkeyedContainer(forKey: key)
try arrayContainer.encode(contentsOf: values)
}
public mutating func encodeArrayIfPresent<T>(_ values: [T]?, forKey key: Self.Key) throws where T : Encodable {
if let values = values {
try encodeArray(values, forKey: key)
}
}
public mutating func encodeMap<T>(_ pairs: [Self.Key: T]) throws where T : Encodable {
for (key, value) in pairs {
try encode(value, forKey: key)
}
}
public mutating func encodeMapIfPresent<T>(_ pairs: [Self.Key: T]?) throws where T : Encodable {
if let pairs = pairs {
try encodeMap(pairs)
}
}
}
extension KeyedDecodingContainerProtocol {
public func decodeArray<T>(_ type: T.Type, forKey key: Self.Key) throws -> [T] where T : Decodable {
var tmpArray = [T]()
var nestedContainer = try nestedUnkeyedContainer(forKey: key)
while !nestedContainer.isAtEnd {
let arrayValue = try nestedContainer.decode(T.self)
tmpArray.append(arrayValue)
}
return tmpArray
}
public func decodeArrayIfPresent<T>(_ type: T.Type, forKey key: Self.Key) throws -> [T]? where T : Decodable {
var tmpArray: [T]? = nil
if contains(key) {
tmpArray = try decodeArray(T.self, forKey: key)
}
return tmpArray
}
public func decodeMap<T>(_ type: T.Type, excludedKeys: Set<Self.Key>) throws -> [Self.Key: T] where T : Decodable {
var map: [Self.Key : T] = [:]
for key in allKeys {
if !excludedKeys.contains(key) {
let value = try decode(T.self, forKey: key)
map[key] = value
}
}
return map
}
}
{{#usePromiseKit}}extension RequestBuilder {
public func execute() -> Promise<Response<T>> {
let deferred = Promise<Response<T>>.pending()

View File

@@ -21,10 +21,7 @@ public enum {{classname}}: {{dataType}}, Codable {
}
{{/isEnum}}
{{^isEnum}}
{{#vars.isEmpty}}
public typealias {{classname}} = {{dataType}}
{{/vars.isEmpty}}
{{^vars.isEmpty}}
open class {{classname}}: {{#parent}}{{{parent}}}{{/parent}}{{^parent}}Codable{{/parent}} {
{{#vars}}
@@ -37,11 +34,11 @@ open class {{classname}}: {{#parent}}{{{parent}}}{{/parent}}{{^parent}}Codable{{
{{#vars}}
{{#isEnum}}
{{#description}}/** {{description}} */
{{/description}}public var {{name}}: {{{datatypeWithEnum}}}{{^unwrapRequired}}?{{/unwrapRequired}}{{#unwrapRequired}}{{^required}}?{{/required}}{{/unwrapRequired}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}
{{/description}}public var {{name}}: {{{datatypeWithEnum}}}{{^required}}?{{/required}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}
{{/isEnum}}
{{^isEnum}}
{{#description}}/** {{description}} */
{{/description}}public var {{name}}: {{{datatype}}}{{^unwrapRequired}}?{{/unwrapRequired}}{{#unwrapRequired}}{{^required}}?{{/required}}{{/unwrapRequired}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}{{#objcCompatible}}{{#vendorExtensions.x-swift-optional-scalar}}
{{/description}}public var {{name}}: {{{datatype}}}{{^required}}?{{/required}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}{{#objcCompatible}}{{#vendorExtensions.x-swift-optional-scalar}}
public var {{name}}Num: NSNumber? {
get {
return {{name}}.map({ return NSNumber(value: $0) })
@@ -51,19 +48,9 @@ open class {{classname}}: {{#parent}}{{{parent}}}{{/parent}}{{^parent}}Codable{{
{{/vars}}
{{#additionalPropertiesType}}
public var additionalProperties: [AnyHashable:{{{additionalPropertiesType}}}] = [:]
public var additionalProperties: [String:{{{additionalPropertiesType}}}] = [:]
{{/additionalPropertiesType}}
{{^unwrapRequired}}
{{^parent}}public init() {}{{/parent}}{{/unwrapRequired}}
{{#unwrapRequired}}
public init({{#allVars}}{{^-first}}, {{/-first}}{{name}}: {{#isEnum}}{{datatypeWithEnum}}{{/isEnum}}{{^isEnum}}{{datatype}}{{/isEnum}}{{^required}}?=nil{{/required}}{{/allVars}}) {
{{#vars}}
self.{{name}} = {{name}}
{{/vars}}
}{{/unwrapRequired}}
{{#additionalPropertiesType}}
public subscript(key: AnyHashable) -> {{{additionalPropertiesType}}}? {
public subscript(key: String) -> {{{additionalPropertiesType}}}? {
get {
if let value = additionalProperties[key] {
return value
@@ -77,12 +64,38 @@ open class {{classname}}: {{#parent}}{{{parent}}}{{/parent}}{{^parent}}Codable{{
}
{{/additionalPropertiesType}}
private enum CodingKeys: String, CodingKey { {{#vars}}
case {{{name}}} = "{{{baseName}}}"{{/vars}}
// Encodable protocol methods
public {{#parent}}override {{/parent}}func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: String.self)
{{#vars}}
try container.encode{{#isListContainer}}Array{{/isListContainer}}{{^required}}IfPresent{{/required}}({{{name}}}, forKey: "{{{baseName}}}")
{{/vars}}
{{#additionalPropertiesType}}
try container.encodeMap(additionalProperties)
{{/additionalPropertiesType}}
}
// Decodable protocol methods
public {{#parent}}override {{/parent}}required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: String.self)
{{#vars}}
{{name}} = try container.decode{{#isListContainer}}Array{{/isListContainer}}{{^required}}IfPresent{{/required}}({{#isListContainer}}{{{items.datatype}}}{{/isListContainer}}{{^isListContainer}}{{{datatype}}}{{/isListContainer}}.self, forKey: "{{{baseName}}}")
{{/vars}}
{{#additionalPropertiesType}}
var nonAdditionalPropertyKeys = Set<String>()
{{#vars}}
nonAdditionalPropertyKeys.insert("{{{baseName}}}")
{{/vars}}
additionalProperties = try container.decodeMap({{{additionalPropertiesType}}}.self, excludedKeys: nonAdditionalPropertyKeys)
{{/additionalPropertiesType}}
}
}
{{/vars.isEmpty}}
{{/isEnum}}
{{/isArrayModel}}
{{/model}}

View File

@@ -274,6 +274,72 @@
}
},
"description": "Response object containing AllPrimitives object"
},
"ModelWithStringAdditionalPropertiesOnly": {
"description": "This is an empty model with no properties and only additionalProperties of type string",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"ModelWithIntAdditionalPropertiesOnly": {
"description": "This is an empty model with no properties and only additionalProperties of type int32",
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
},
"ModelWithPropertiesAndAdditionalProperties": {
"description": "This is an empty model with no properties and only additionalProperties of type int32",
"type": "object",
"required": [
"myIntegerReq",
"myPrimitiveReq",
"myStringArrayReq",
"myPrimitiveArrayReq"
],
"properties": {
"myIntegerReq": {
"type": "integer"
},
"myIntegerOpt": {
"type": "integer"
},
"myPrimitiveReq": {
"$ref": "#/definitions/AllPrimitives"
},
"myPrimitiveOpt": {
"$ref": "#/definitions/AllPrimitives"
},
"myStringArrayReq": {
"type": "array",
"items": {
"type": "string"
}
},
"myStringArrayOpt": {
"type": "array",
"items": {
"type": "string"
}
},
"myPrimitiveArrayReq": {
"type": "array",
"items": {
"$ref": "#/definitions/AllPrimitives"
}
},
"myPrimitiveArrayOpt": {
"type": "array",
"items": {
"$ref": "#/definitions/AllPrimitives"
}
}
},
"additionalProperties": {
"type": "string"
}
}
}
}