Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@darkbrewx
Copy link

Issue Link πŸ”—

#3870

Goals ⚽

Enable different nil handling behaviors for encode() vs encodeIfPresent() in URLEncodedFormEncoder. Adds .intentionalOnly strategy that matches JSONEncoder semantics while maintaining full backward compatibility.

Implementation Details 🚧

Core Changes:

  • Extended NilEncoding with dual strategy support via init(encode:encodeIfPresent:)
  • Added encodeNilIfPresent() method and updated KeyedContainer._encodeIfPresent() to use it
  • New .intentionalOnly strategy: encodes nil as "null" for encode(), skips for encodeIfPresent()

Usage:

let encoder = URLEncodedFormEncoder(nilEncoding: .intentionalOnly)

struct UserData: Encodable {
    let name: String? = nil
    let age: Int? = nil
    
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)        // β†’ "name=null"  
        try container.encodeIfPresent(age, forKey: .age) // β†’ skipped
    }
    
    enum CodingKeys: String, CodingKey { case name, age }
}
// Result: "name=null"

Backward compatibility: Existing init(encoding:) API unchanged.

Testing Details πŸ”

New Tests: 10 test cases in IntentionalNilEncodingTests

  • βœ… .intentionalOnly strategy behavior
  • βœ… Custom dual strategies
  • βœ… Array recursion and edge cases
  • βœ… Backward compatibility

Results: All tests pass, existing URLEncodedFormEncoder tests unaffected.

- Implement dual strategy pattern for nil encoding with separate behaviors for encode() and encodeIfPresent()
- Add intentionalOnly strategy that encodes nil as 'null' for explicit encode() calls while skipping nil for encodeIfPresent()
- Support custom dual strategies through new NilEncoding initializer with encode/encodeIfPresent parameters
- Maintain backward compatibility with existing single-strategy API
- Add comprehensive test coverage including array recursion, mixed scenarios, and edge cases
- Enable JSONEncoder-compatible nil handling for form-encoded requests
Copy link
Contributor

@jshier jshier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! This looks like a good approach, but you need to be more careful about source and behavioral changes before it can be merged. Please ensure no source or behavior changes are visible to consumers of the API. Additionally, you can consolidate the added tests into the existing encoder tests.

public struct NilEncoding: Sendable {
/// Encodes `nil` by dropping the entire key / value pair.
public static let dropKey = NilEncoding { nil }
public static let dropKey = NilEncoding(encoding: { nil })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this is a source breaking change and can't be required. Keeping the existing init around is a good first step, but you need to ensure there's no ambiguity between the old and new versions when using the unlabeled form.

Copy link
Author

@darkbrewx darkbrewx Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, may I ask what you mean by β€œambiguity”?

  1. I haven’t modified the signature of the existing initializer.
  2. The new initializer takes 2 parameters, while the existing one only takes 1.
  3. Also, the parameter names are different.

The change here, adding parameter labels, I thought it will make more explicit and easier to understand, while keeping the same behavior as the original trailing closure.

/// - encode: Strategy used when `encode(nil)` or `encodeNil()` is called directly
/// - encodeIfPresent: Strategy used when `encodeIfPresent(nil)` is called
public init(
encode: @escaping @Sendable () -> String?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first parameter could be encodeNil while the second remains encodeIfPresent, just to make it a bit more clear.

private let encodeEncoding: @Sendable () -> String?
private let encodeIfPresentEncoding: @Sendable () -> String?

/// Creates a NilEncoding with separate strategies for encode and encodeIfPresent methods.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use tick marks ("`") for type and parameter names in docs.

} else {
try encodeNil(forKey: key)
// Use the encodeIfPresent strategy for nil values
guard let nilValue = nilEncoding.encodeNilIfPresent() else { return }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior can't change either. Instead it needs to default to the current behavior and only use the new behavior if the user provides the encodeIfPresent closure.

Copy link
Author

@darkbrewx darkbrewx Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing this out.
From my perspective, the current behavior remains fully backward-compatible.

  1. When using the single-parameter initializer, the encodeIfPresentEncoding and encodeEncoding closures are equivalent.
  2. Here calling nilEncoding.encodeNilIfPresent() can fall back to nilEncoding.encodeNil() automatically.

Implementing it as you suggested would require to:

  1. changingencodeIfPresentEncoding to optional, and when using single-parameter initializer, set it as nil.
  2. exposing encodeIfPresentEncoding as fileprivate or introducing an additional fileprivate(wider than private) flag to check for nil; otherwise, cannot directly unwrap it using if let.

So, may I ask you whether the current implementation is acceptable? Or if you have any further concerns, please let me know.

//
// IntentionalNilEncodingTests.swift
//
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All new files should use the current year for copyright.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really, though, these tests should be consolidated into the existing encoding tests.

final class IntentionalNilEncodingTests: BaseTestCase {
// MARK: - Test Helper Structures

struct TestStruct: Encodable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to consolidate all of the test structs as fileprivates at the bottom of this file.

- Integrate nil encoding tests into existing ParameterEncoderTests following project patterns
- Use backticks for proper variable and value documentation formatting
- Rename parameters for better readability and understanding
- Remove standalone test file in favor of consolidated test structure
@darkbrewx darkbrewx requested a review from jshier September 18, 2025 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants