Кодировать значение nil как null с помощью JSONEncoder

Я использую Swift 4 JSONEncoder. У меня есть структура Codable с необязательным свойством, и я хотел бы, чтобы это свойство отображалось как значение null в произведенных данных JSON, когда значение nil. Однако JSONEncoder отбрасывает свойство и не добавляет его к выходу JSON. Есть ли способ настроить JSONEncoder так, чтобы он сохранял ключ и устанавливал его в null в этом случае?

Пример

Ниже приведен фрагмент кода {"number":1}, но я бы хотел, чтобы он дал мне {"string":null,"number":1}:

struct Foo: Codable {
  var string: String? = nil
  var number: Int = 1
}

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

Ответ 1

Да, но вам придется написать свой собственный кодер; вы не можете использовать значение по умолчанию.

struct Foo: Codable {
    var string: String? = nil
    var number: Int = 1

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(number, forKey: .number)
        try container.encode(string, forKey: .string)
    }
}

Кодирование необязательного напрямую будет кодировать нуль, как вы ищите.

Если это важный прецедент для вас, вы можете рассмотреть возможность открытия дефекта bugs.swift.org, чтобы запросить новый OptionalEncodingStrategy флаг, который будет добавлен в JSONEncoder, чтобы соответствовать существующим DateEncodingStrategy и т.д. (см. ниже, почему это, вероятно, невозможно реализовать в Swift сегодня, но попасть в систему отслеживания по-прежнему полезно по мере развития Swift.)


Изменить: к вопросам Пауло ниже, это отправляется в общую версию encode<T: Encodable>, потому что Optional соответствует Encodable. Это реализовано в Codable.swift следующим образом:

extension Optional : Encodable /* where Wrapped : Encodable */ {
    @_inlineable // FIXME(sil-serialize-all)
    public func encode(to encoder: Encoder) throws {
        assertTypeIsEncodable(Wrapped.self, in: type(of: self))

        var container = encoder.singleValueContainer()
        switch self {
        case .none: try container.encodeNil()
        case .some(let wrapped): try (wrapped as! Encodable).__encode(to: &container)
        }
    }
}

Это завершает вызов encodeNil, и я думаю, что позволить stdlib обрабатывать опции как просто еще один кодирующий лучше, чем рассматривать их как особый случай в нашем собственном кодере и вызывать encodeNil сами.

Еще один очевидный вопрос - почему он работает таким образом, в первую очередь. Поскольку параметр Необязательный кодируется, а сгенерированное кодируемое соответствие кодирует все свойства, почему "кодировать все свойства вручную" работают по-другому? Ответ заключается в том, что генератор соответствия включает специальный случай для опций:

// Now need to generate `try container.encode(x, forKey: .x)` for all
// existing properties. Optional properties get `encodeIfPresent`.
...

if (varType->getAnyNominal() == C.getOptionalDecl() ||
    varType->getAnyNominal() == C.getImplicitlyUnwrappedOptionalDecl()) {
  methodName = C.Id_encodeIfPresent;
}

Это означает, что для изменения этого поведения потребуется изменить автоматически сгенерированное соответствие, а не JSONEncoder (что также означает, что, вероятно, действительно сложно сделать настраиваемым в настоящее время Swift....)