Skip to content

Commit b807f6f

Browse files
ehychewing328
authored andcommitted
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") } }
1 parent 8067612 commit b807f6f

40 files changed

+814
-247
lines changed

modules/swagger-codegen/src/main/resources/swift4/Extensions.mustache

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,92 @@ extension UUID: JSONEncodable {
8585
}
8686
}
8787

88+
extension String: CodingKey {
89+
90+
public var stringValue: String {
91+
return self
92+
}
93+
94+
public init?(stringValue: String) {
95+
self.init(stringLiteral: stringValue)
96+
}
97+
98+
public var intValue: Int? {
99+
return nil
100+
}
101+
102+
public init?(intValue: Int) {
103+
return nil
104+
}
105+
106+
}
107+
108+
extension KeyedEncodingContainerProtocol {
109+
110+
public mutating func encodeArray<T>(_ values: [T], forKey key: Self.Key) throws where T : Encodable {
111+
var arrayContainer = nestedUnkeyedContainer(forKey: key)
112+
try arrayContainer.encode(contentsOf: values)
113+
}
114+
115+
public mutating func encodeArrayIfPresent<T>(_ values: [T]?, forKey key: Self.Key) throws where T : Encodable {
116+
if let values = values {
117+
try encodeArray(values, forKey: key)
118+
}
119+
}
120+
121+
public mutating func encodeMap<T>(_ pairs: [Self.Key: T]) throws where T : Encodable {
122+
for (key, value) in pairs {
123+
try encode(value, forKey: key)
124+
}
125+
}
126+
127+
public mutating func encodeMapIfPresent<T>(_ pairs: [Self.Key: T]?) throws where T : Encodable {
128+
if let pairs = pairs {
129+
try encodeMap(pairs)
130+
}
131+
}
132+
133+
}
134+
135+
extension KeyedDecodingContainerProtocol {
136+
137+
public func decodeArray<T>(_ type: T.Type, forKey key: Self.Key) throws -> [T] where T : Decodable {
138+
var tmpArray = [T]()
139+
140+
var nestedContainer = try nestedUnkeyedContainer(forKey: key)
141+
while !nestedContainer.isAtEnd {
142+
let arrayValue = try nestedContainer.decode(T.self)
143+
tmpArray.append(arrayValue)
144+
}
145+
146+
return tmpArray
147+
}
148+
149+
public func decodeArrayIfPresent<T>(_ type: T.Type, forKey key: Self.Key) throws -> [T]? where T : Decodable {
150+
var tmpArray: [T]? = nil
151+
152+
if contains(key) {
153+
tmpArray = try decodeArray(T.self, forKey: key)
154+
}
155+
156+
return tmpArray
157+
}
158+
159+
public func decodeMap<T>(_ type: T.Type, excludedKeys: Set<Self.Key>) throws -> [Self.Key: T] where T : Decodable {
160+
var map: [Self.Key : T] = [:]
161+
162+
for key in allKeys {
163+
if !excludedKeys.contains(key) {
164+
let value = try decode(T.self, forKey: key)
165+
map[key] = value
166+
}
167+
}
168+
169+
return map
170+
}
171+
172+
}
173+
88174
{{#usePromiseKit}}extension RequestBuilder {
89175
public func execute() -> Promise<Response<T>> {
90176
let deferred = Promise<Response<T>>.pending()

modules/swagger-codegen/src/main/resources/swift4/model.mustache

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ public enum {{classname}}: {{dataType}}, Codable {
2121
}
2222
{{/isEnum}}
2323
{{^isEnum}}
24-
{{#vars.isEmpty}}
25-
public typealias {{classname}} = {{dataType}}
26-
{{/vars.isEmpty}}
27-
{{^vars.isEmpty}}
24+
2825
open class {{classname}}: {{#parent}}{{{parent}}}{{/parent}}{{^parent}}Codable{{/parent}} {
2926
3027
{{#vars}}
@@ -37,11 +34,11 @@ open class {{classname}}: {{#parent}}{{{parent}}}{{/parent}}{{^parent}}Codable{{
3734
{{#vars}}
3835
{{#isEnum}}
3936
{{#description}}/** {{description}} */
40-
{{/description}}public var {{name}}: {{{datatypeWithEnum}}}{{^unwrapRequired}}?{{/unwrapRequired}}{{#unwrapRequired}}{{^required}}?{{/required}}{{/unwrapRequired}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}
37+
{{/description}}public var {{name}}: {{{datatypeWithEnum}}}{{^required}}?{{/required}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}
4138
{{/isEnum}}
4239
{{^isEnum}}
4340
{{#description}}/** {{description}} */
44-
{{/description}}public var {{name}}: {{{datatype}}}{{^unwrapRequired}}?{{/unwrapRequired}}{{#unwrapRequired}}{{^required}}?{{/required}}{{/unwrapRequired}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}{{#objcCompatible}}{{#vendorExtensions.x-swift-optional-scalar}}
41+
{{/description}}public var {{name}}: {{{datatype}}}{{^required}}?{{/required}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}{{#objcCompatible}}{{#vendorExtensions.x-swift-optional-scalar}}
4542
public var {{name}}Num: NSNumber? {
4643
get {
4744
return {{name}}.map({ return NSNumber(value: $0) })
@@ -51,19 +48,9 @@ open class {{classname}}: {{#parent}}{{{parent}}}{{/parent}}{{^parent}}Codable{{
5148
{{/vars}}
5249

5350
{{#additionalPropertiesType}}
54-
public var additionalProperties: [AnyHashable:{{{additionalPropertiesType}}}] = [:]
51+
public var additionalProperties: [String:{{{additionalPropertiesType}}}] = [:]
5552

56-
{{/additionalPropertiesType}}
57-
{{^unwrapRequired}}
58-
{{^parent}}public init() {}{{/parent}}{{/unwrapRequired}}
59-
{{#unwrapRequired}}
60-
public init({{#allVars}}{{^-first}}, {{/-first}}{{name}}: {{#isEnum}}{{datatypeWithEnum}}{{/isEnum}}{{^isEnum}}{{datatype}}{{/isEnum}}{{^required}}?=nil{{/required}}{{/allVars}}) {
61-
{{#vars}}
62-
self.{{name}} = {{name}}
63-
{{/vars}}
64-
}{{/unwrapRequired}}
65-
{{#additionalPropertiesType}}
66-
public subscript(key: AnyHashable) -> {{{additionalPropertiesType}}}? {
53+
public subscript(key: String) -> {{{additionalPropertiesType}}}? {
6754
get {
6855
if let value = additionalProperties[key] {
6956
return value
@@ -77,12 +64,38 @@ open class {{classname}}: {{#parent}}{{{parent}}}{{/parent}}{{^parent}}Codable{{
7764
}
7865
{{/additionalPropertiesType}}
7966

80-
private enum CodingKeys: String, CodingKey { {{#vars}}
81-
case {{{name}}} = "{{{baseName}}}"{{/vars}}
67+
// Encodable protocol methods
68+
69+
public {{#parent}}override {{/parent}}func encode(to encoder: Encoder) throws {
70+
71+
var container = encoder.container(keyedBy: String.self)
72+
73+
{{#vars}}
74+
try container.encode{{#isListContainer}}Array{{/isListContainer}}{{^required}}IfPresent{{/required}}({{{name}}}, forKey: "{{{baseName}}}")
75+
{{/vars}}
76+
{{#additionalPropertiesType}}
77+
try container.encodeMap(additionalProperties)
78+
{{/additionalPropertiesType}}
8279
}
8380

81+
// Decodable protocol methods
82+
83+
public {{#parent}}override {{/parent}}required init(from decoder: Decoder) throws {
84+
let container = try decoder.container(keyedBy: String.self)
85+
86+
{{#vars}}
87+
{{name}} = try container.decode{{#isListContainer}}Array{{/isListContainer}}{{^required}}IfPresent{{/required}}({{#isListContainer}}{{{items.datatype}}}{{/isListContainer}}{{^isListContainer}}{{{datatype}}}{{/isListContainer}}.self, forKey: "{{{baseName}}}")
88+
{{/vars}}
89+
{{#additionalPropertiesType}}
90+
var nonAdditionalPropertyKeys = Set<String>()
91+
{{#vars}}
92+
nonAdditionalPropertyKeys.insert("{{{baseName}}}")
93+
{{/vars}}
94+
additionalProperties = try container.decodeMap({{{additionalPropertiesType}}}.self, excludedKeys: nonAdditionalPropertyKeys)
95+
{{/additionalPropertiesType}}
96+
}
8497
}
85-
{{/vars.isEmpty}}
98+
8699
{{/isEnum}}
87100
{{/isArrayModel}}
88101
{{/model}}

modules/swagger-codegen/src/test/resources/2_0/swift4Test.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,72 @@
274274
}
275275
},
276276
"description": "Response object containing AllPrimitives object"
277+
},
278+
"ModelWithStringAdditionalPropertiesOnly": {
279+
"description": "This is an empty model with no properties and only additionalProperties of type string",
280+
"type": "object",
281+
"additionalProperties": {
282+
"type": "string"
283+
}
284+
},
285+
"ModelWithIntAdditionalPropertiesOnly": {
286+
"description": "This is an empty model with no properties and only additionalProperties of type int32",
287+
"type": "object",
288+
"additionalProperties": {
289+
"type": "integer",
290+
"format": "int32"
291+
}
292+
},
293+
"ModelWithPropertiesAndAdditionalProperties": {
294+
"description": "This is an empty model with no properties and only additionalProperties of type int32",
295+
"type": "object",
296+
"required": [
297+
"myIntegerReq",
298+
"myPrimitiveReq",
299+
"myStringArrayReq",
300+
"myPrimitiveArrayReq"
301+
],
302+
"properties": {
303+
"myIntegerReq": {
304+
"type": "integer"
305+
},
306+
"myIntegerOpt": {
307+
"type": "integer"
308+
},
309+
"myPrimitiveReq": {
310+
"$ref": "#/definitions/AllPrimitives"
311+
},
312+
"myPrimitiveOpt": {
313+
"$ref": "#/definitions/AllPrimitives"
314+
},
315+
"myStringArrayReq": {
316+
"type": "array",
317+
"items": {
318+
"type": "string"
319+
}
320+
},
321+
"myStringArrayOpt": {
322+
"type": "array",
323+
"items": {
324+
"type": "string"
325+
}
326+
},
327+
"myPrimitiveArrayReq": {
328+
"type": "array",
329+
"items": {
330+
"$ref": "#/definitions/AllPrimitives"
331+
}
332+
},
333+
"myPrimitiveArrayOpt": {
334+
"type": "array",
335+
"items": {
336+
"$ref": "#/definitions/AllPrimitives"
337+
}
338+
}
339+
},
340+
"additionalProperties": {
341+
"type": "string"
342+
}
277343
}
278344
}
279345
}

samples/client/test/swift4/default/TestClient.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ Pod::Spec.new do |s|
88
s.license = 'Proprietary'
99
s.homepage = 'https://github.com/swagger-api/swagger-codegen'
1010
s.summary = 'TestClient'
11-
s.source_files = 'TestClient/Classes/Swaggers/**/*.swift'
12-
s.dependency 'Alamofire', '~> 4.5'
11+
s.source_files = 'TestClient/Classes/**/*.swift'
12+
s.dependency 'Alamofire', '~> 4.5.0'
1313
end

samples/client/test/swift4/default/TestClient/Classes/Swaggers/APIs/Swift4TestAPI.swift

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,60 +30,85 @@ open class Swift4TestAPI {
3030
- This endpoint tests get a dictionary which contains examples of all of the models.
3131
- examples: [{contentType=application/json, example={
3232
"myPrimitive" : {
33-
"myDateTimeArray" : [ "2000-01-23T04:56:07.000+00:00" ],
34-
"myStringArray" : [ "aeiou" ],
33+
"myDateTimeArray" : [ "2000-01-23T04:56:07.000+00:00", "2000-01-23T04:56:07.000+00:00" ],
34+
"myStringArray" : [ "myStringArray", "myStringArray" ],
3535
"myFile" : "",
36-
"myFloatArray" : [ 2.302136 ],
37-
"myBytes" : "aeiou",
36+
"myFloatArray" : [ 2.302136, 2.302136 ],
37+
"myBytes" : "myBytes",
3838
"myLong" : 1,
39-
"myBooleanArray" : [ true ],
40-
"myDoubleArray" : [ 9.301444243932576 ],
39+
"myBooleanArray" : [ true, true ],
40+
"myDoubleArray" : [ 9.301444243932576, 9.301444243932576 ],
4141
"myInteger" : 0,
42-
"myString" : "aeiou",
43-
"myBytesArray" : [ "aeiou" ],
42+
"myString" : "myString",
43+
"myBytesArray" : [ "myBytesArray", "myBytesArray" ],
4444
"myDouble" : 7.061401241503109,
4545
"myDate" : "2000-01-23",
46-
"myDateArray" : [ "2000-01-23" ],
46+
"myDateArray" : [ "2000-01-23", "2000-01-23" ],
4747
"myDateTime" : "2000-01-23T04:56:07.000+00:00",
48-
"myLongArray" : [ 5 ],
49-
"myIntegerArray" : [ 6 ],
48+
"myLongArray" : [ 5, 5 ],
49+
"myIntegerArray" : [ 6, 6 ],
5050
"myUUID" : "046b6c7f-0b8a-43b9-b35d-6489e6daee91",
5151
"myBoolean" : true,
52-
"myFileArray" : [ "" ],
52+
"myFileArray" : [ "", "" ],
5353
"myStringEnum" : { },
5454
"myFloat" : 5.637377,
55-
"myStringEnumArray" : [ null ],
56-
"myUUIDArray" : [ "046b6c7f-0b8a-43b9-b35d-6489e6daee91" ]
55+
"myStringEnumArray" : [ null, null ],
56+
"myUUIDArray" : [ "046b6c7f-0b8a-43b9-b35d-6489e6daee91", "046b6c7f-0b8a-43b9-b35d-6489e6daee91" ]
5757
},
5858
"myVariableNameTest" : {
59-
"for" : "aeiou",
60-
"example_name" : "aeiou"
59+
"for" : "for",
60+
"example_name" : "example_name"
6161
},
6262
"myPrimitiveArray" : [ {
63-
"myDateTimeArray" : [ "2000-01-23T04:56:07.000+00:00" ],
64-
"myStringArray" : [ "aeiou" ],
63+
"myDateTimeArray" : [ "2000-01-23T04:56:07.000+00:00", "2000-01-23T04:56:07.000+00:00" ],
64+
"myStringArray" : [ "myStringArray", "myStringArray" ],
6565
"myFile" : "",
66-
"myFloatArray" : [ 2.302136 ],
67-
"myBytes" : "aeiou",
66+
"myFloatArray" : [ 2.302136, 2.302136 ],
67+
"myBytes" : "myBytes",
6868
"myLong" : 1,
69-
"myBooleanArray" : [ true ],
70-
"myDoubleArray" : [ 9.301444243932576 ],
69+
"myBooleanArray" : [ true, true ],
70+
"myDoubleArray" : [ 9.301444243932576, 9.301444243932576 ],
7171
"myInteger" : 0,
72-
"myString" : "aeiou",
73-
"myBytesArray" : [ "aeiou" ],
72+
"myString" : "myString",
73+
"myBytesArray" : [ "myBytesArray", "myBytesArray" ],
7474
"myDouble" : 7.061401241503109,
7575
"myDate" : "2000-01-23",
76-
"myDateArray" : [ "2000-01-23" ],
76+
"myDateArray" : [ "2000-01-23", "2000-01-23" ],
7777
"myDateTime" : "2000-01-23T04:56:07.000+00:00",
78-
"myLongArray" : [ 5 ],
79-
"myIntegerArray" : [ 6 ],
78+
"myLongArray" : [ 5, 5 ],
79+
"myIntegerArray" : [ 6, 6 ],
8080
"myUUID" : "046b6c7f-0b8a-43b9-b35d-6489e6daee91",
8181
"myBoolean" : true,
82-
"myFileArray" : [ "" ],
82+
"myFileArray" : [ "", "" ],
8383
"myStringEnum" : { },
8484
"myFloat" : 5.637377,
85-
"myStringEnumArray" : [ null ],
86-
"myUUIDArray" : [ "046b6c7f-0b8a-43b9-b35d-6489e6daee91" ]
85+
"myStringEnumArray" : [ null, null ],
86+
"myUUIDArray" : [ "046b6c7f-0b8a-43b9-b35d-6489e6daee91", "046b6c7f-0b8a-43b9-b35d-6489e6daee91" ]
87+
}, {
88+
"myDateTimeArray" : [ "2000-01-23T04:56:07.000+00:00", "2000-01-23T04:56:07.000+00:00" ],
89+
"myStringArray" : [ "myStringArray", "myStringArray" ],
90+
"myFile" : "",
91+
"myFloatArray" : [ 2.302136, 2.302136 ],
92+
"myBytes" : "myBytes",
93+
"myLong" : 1,
94+
"myBooleanArray" : [ true, true ],
95+
"myDoubleArray" : [ 9.301444243932576, 9.301444243932576 ],
96+
"myInteger" : 0,
97+
"myString" : "myString",
98+
"myBytesArray" : [ "myBytesArray", "myBytesArray" ],
99+
"myDouble" : 7.061401241503109,
100+
"myDate" : "2000-01-23",
101+
"myDateArray" : [ "2000-01-23", "2000-01-23" ],
102+
"myDateTime" : "2000-01-23T04:56:07.000+00:00",
103+
"myLongArray" : [ 5, 5 ],
104+
"myIntegerArray" : [ 6, 6 ],
105+
"myUUID" : "046b6c7f-0b8a-43b9-b35d-6489e6daee91",
106+
"myBoolean" : true,
107+
"myFileArray" : [ "", "" ],
108+
"myStringEnum" : { },
109+
"myFloat" : 5.637377,
110+
"myStringEnumArray" : [ null, null ],
111+
"myUUIDArray" : [ "046b6c7f-0b8a-43b9-b35d-6489e6daee91", "046b6c7f-0b8a-43b9-b35d-6489e6daee91" ]
87112
} ]
88113
}}]
89114

samples/client/test/swift4/default/TestClient/Classes/Swaggers/CodableHelper.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ open class CodableHelper {
1616
var returnedError: Error? = nil
1717

1818
let decoder = JSONDecoder()
19-
decoder.dataDecodingStrategy = .base64Decode
19+
decoder.dataDecodingStrategy = .base64
2020
if #available(iOS 10.0, *) {
2121
decoder.dateDecodingStrategy = .iso8601
2222
}
@@ -38,7 +38,7 @@ open class CodableHelper {
3838
if prettyPrint {
3939
encoder.outputFormatting = .prettyPrinted
4040
}
41-
encoder.dataEncodingStrategy = .base64Encode
41+
encoder.dataEncodingStrategy = .base64
4242
if #available(iOS 10.0, *) {
4343
encoder.dateEncodingStrategy = .iso8601
4444
}

0 commit comments

Comments
 (0)