Skip to content
GitLab
Projects Groups Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in / Register
  • O openapi-generator
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
  • Issues 3,476
    • Issues 3,476
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 402
    • Merge requests 402
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Infrastructure Registry
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • OpenAPI Tools
  • openapi-generator
  • Issues
  • #11033
Closed
Open
Issue created Dec 04, 2021 by Administrator@rootContributor

[REQ] [Swift5] Fully control encoding of optional parameter

Created by: jarrodparkes

Is your feature request related to a problem? Please describe.

Problem 1

Consider an endpoint called PATCH /task/{task-id} — this comes from an internal API that I use. PATCH /task/{task-id} states that passing null for any task property in the request body will "clear the value".

Here's the spec for the payload...

{
  "title": "TaskPatchPayload",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "nullable": true
    },
    "due_at": {
      "type": "string",
      "format": "date-time"
      "nullable": true
    },
    "completed_at": {
      "type": "string",
      "format": "date-time"
      "nullable": true
    }
  }
}

Written as-is, none of the properties are required, but each is nullable. This means the generated model would look like this...

struct TaskPatchPayload: Codable {
    let name: String?
    let dueAt: Date?
    let completedAt: Date?

    // ... CodingKeys redacted

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(name, forKey: .name)
        try container.encodeIfPresent(dueAt, forKey: .dueAt)
        try container.encodeIfPresent(completedAt, forKey: .completedAt)
    }
}

The problem with this example is that null can never be explicitly encoded, so you can never "clear the value" of a property. encodeIfPresent only encodes a value if it is non-nil. Stumbling into this problem is what caused me to write #10926 (closed). But, after closer examination, you can explicitly encode null. Stay with me.

Problem 2

If you modify the model spec so that properties are nullable AND required, then the generated model will allow null to be explicitly encoded. Here's what the spec would look like...

{
  "title": "TaskPatchPayload",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "nullable": true
    },
    "due_at": {
      "type": "string",
      "format": "date-time"
      "nullable": true
    },
    "completed_at": {
      "type": "string",
      "format": "date-time"
      "nullable": true
    }
  },
  "required": ["name", "due_at", "completed_at"]
}

And the generated model...

struct TaskPatchPayload: Codable {
    let name: String?
    let dueAt: Date?
    let completedAt: Date?

    // ... CodingKeys redacted 

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(dueAt, forKey: .dueAt)
        try container.encode(completedAt, forKey: .completedAt)
    }
}

In this model encode is used, so the property is encoded regardless of whether it is nil or non-nil. Problem solved, right? Well... not quite. Remember, this payload is for PATCH /task/{task-id}, and this API and many reputable sources state...

The HTTP PATCH request method applies partial modifications to a resource.

But, since every property in this model is always encoded, you can never do a partial modification. For example, your payloads might look like this...

{
  "name": null,
  "due_at": null,
  "completed_at": null
}
{
  "name": "My new task",
  "due_at": null,
  "completed_at": "2021-12-03T21:30:00Z"
}

In each example, every property is present. This isn't a PATCH and you also have the adverse effect of null properties "clearing a value" even when that may not be your intention. So now what?

Describe the solution you'd like

I'd like to propose a simple property extension (ex: x-explicit-null-encodable) and a corresponding custom type that will generate models that give developers full control of how/when they want to encode null.

I've got some sample code running in a Playground that already looks promising (full example):

/// A value that can be included in a payload (`.explicitNone` or `.some`)
/// or completely absent (`.none`). Intended for request payloads.
public enum ExplicitNullEncodable<Wrapped> {
    case none
    case explicitNone
    case some(Wrapped)
}

extension ExplicitNullEncodable: Codable where Wrapped: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let value = try? container.decode(Wrapped.self) {
            self = .some(value)
        } else if container.decodeNil() {
            self = .explicitNone
        } else {
            self = .none
        }
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .none: return
        case .explicitNone: try container.encodeNil()
        case .some(let wrapped): try container.encode(wrapped)
        }
    }
}

struct TaskPatchPayload: Codable {
    let name: ExplicitNullEncodable<String>
    let dueAt: ExplicitNullEncodable<Date>
    let completedAt: ExplicitNullEncodable<Date>
    
    public enum CodingKeys: String, CodingKey, CaseIterable {
        case name = "name"
        case dueAt = "due_at"
        case completedAt = "completed_at"
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        switch name {
        case .none:
            break
        case .explicitNone, .some:
            try container.encode(name, forKey: .name)
        }

        switch dueAt {
        case .none:
            break
        case .explicitNone, .some:
            try container.encode(dueAt, forKey: .dueAt)
        }

        switch completedAt {
        case .none:
            break
        case .explicitNone, .some:
            try container.encode(completedAt, forKey: .completedAt)
        }
    }
}

Describe alternatives you've considered

  • Always send every property ("use a PUT instead of a PATCH") => possible, but unnecessarily complex for small changes
  • Tell my team to re-write the internal API => not likely

Additional context

I'm proposing a property extension, and not a generator option that would apply this customization to all optionals. I'm doing this because the default handling of optionals covers most cases. An API-wide customization seems too heavy-handed when most don't need this level of control.

Assignee
Assign to
Time tracking