sourcetip

JSONEncoder를 사용하여 null 값을 null로 인코딩합니다.

fileupload 2023. 2. 13. 20:37
반응형

JSONEncoder를 사용하여 null 값을 null로 인코딩합니다.

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)!)

네, 하지만 직접 쓰셔야 해요encode(to:)구현, 자동 생성된 구현은 사용할 수 없습니다.

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)
        }
    }
}

그러면 콜이 로 랩됩니다.encodeNilstdlib가 옵션들을 단지 다른 Encodable로 취급하는 것이 우리의 인코더 및 콜에서 특별한 케이스로 취급하는 것보다 낫다고 생각합니다.encodeNil우리자신.

또 다른 분명한 질문은 애초에 왜 이런 방식으로 작동하느냐는 것이다.Optional은 Encodable이고 Encodable 준거로 생성된 Encodable은 모든 속성을 인코딩하므로 "모든 속성을 수동으로 인코딩"이 다르게 작동하는 이유는 무엇입니까?정답은 적합성 생성기에 옵션의 특수한 케이스가 포함되어 있다는 것입니다.

// 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에서는 설정 가능한 것을 만드는 것은 매우 어려운 일이라는 것입니다.

속성 래퍼를 사용하는 방법은 다음과 같습니다(Swift v5.1 필요).

@propertyWrapper
struct NullEncodable<T>: Encodable where T: Encodable {
    
    var wrappedValue: T?

    init(wrappedValue: T?) {
        self.wrappedValue = wrappedValue
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch wrappedValue {
        case .some(let value): try container.encode(value)
        case .none: try container.encodeNil()
        }
    }
}

사용 예:

struct Tuplet: Encodable {
    let a: String
    let b: Int
    @NullEncodable var c: String? = nil
}

struct Test: Encodable {
    @NullEncodable var name: String? = nil
    @NullEncodable var description: String? = nil
    @NullEncodable var tuplet: Tuplet? = nil
}

var test = Test()
test.tuplet = Tuplet(a: "whee", b: 42)
test.description = "A test"

let data = try JSONEncoder().encode(test)
print(String(data: data, encoding: .utf8) ?? "")

출력:

{
  "name": null,
  "description": "A test",
  "tuplet": {
    "a": "whee",
    "b": 42,
    "c": null
  }
}

완전 실장 : https://github.com/g-mark/NullCodable

다음은 프로젝트에서 사용한 접근법입니다.도움이 됐으면 좋겠다.

struct CustomBody: Codable {
    let method: String
    let params: [Param]

    enum CodingKeys: String, CodingKey {
        case method = "method"
        case params = "params"
    }
}

enum Param: Codable {
    case bool(Bool)
    case integer(Int)
    case string(String)
    case stringArray([String])
    case valueNil
    case unsignedInteger(UInt)
    case optionalString(String?)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Bool.self) {
            self = .bool(x)
            return
        }
        if let x = try? container.decode(Int.self) {
            self = .integer(x)
            return
        }
        if let x = try? container.decode([String].self) {
              self = .stringArray(x)
              return
          }
        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }
        if let x = try? container.decode(UInt.self) {
            self = .unsignedInteger(x)
            return
        }
        throw DecodingError.typeMismatch(Param.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Param"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .bool(let x):
            try container.encode(x)
        case .integer(let x):
            try container.encode(x)
        case .string(let x):
            try container.encode(x)
        case .stringArray(let x):
            try container.encode(x)
        case .valueNil:
            try container.encodeNil()
        case .unsignedInteger(let x):
            try container.encode(x)
        case .optionalString(let x):
            x?.isEmpty == true ? try container.encodeNil() : try container.encode(x)
        }
    }
}

그리고 용도는 이렇습니다.

RequestBody.CustomBody(method: "WSDocMgmt.getDocumentsInContentCategoryBySearchSource", params: [.string(legacyToken), .string(shelfId), .bool(true), .valueNil, .stringArray(queryFrom(filters: filters ?? [])), .optionalString(sortMethodParameters()), .bool(sortMethodAscending()), .unsignedInteger(segment ?? 0), .unsignedInteger(segmentSize ?? 0), .string("NO_PATRON_STATUS")])

동작을 제어하기 위해 이 열거형을 사용합니다.백엔드에서 필요한 작업입니다.

public enum Tristate<Wrapped> : ExpressibleByNilLiteral, Encodable {

/// Null
case none

/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)

/// Pending value, not none, not some
case pending

/// Creates an instance initialized with .pending.
public init() {
    self = .pending
}

/// Creates an instance initialized with .none.
public init(nilLiteral: ()) {
    self = .none
}

/// Creates an instance that stores the given value.
public init(_ some: Wrapped) {
    self = .some(some)
}

public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    switch self {
        case .none:
            try container.encodeNil()
        case .some(let wrapped):
            try (wrapped as! Encodable).encode(to: encoder)
        case .pending: break // do nothing
    }
}

}

typealias TriStateString = Tristate<String>
typealias TriStateInt = Tristate<Int>
typealias TriStateBool = Tristate<Bool>

/// 테스트

struct TestStruct: Encodable {
var variablePending: TriStateString?
var variableSome: TriStateString?
var variableNil: TriStateString?

}

    /// Structure with tristate strings:
    let testStruc = TestStruct(/*variablePending: TriStateString(),*/ // pending, unresolved
                               variableSome: TriStateString("test"), // some, resolved
                               variableNil: TriStateString(nil)) // nil, resolved

    /// Make the structure also tristate
    let tsStruct = Tristate<TestStruct>(testStruc)

    /// Make a json from the structure
    do {
        let jsonData = try JSONEncoder().encode(tsStruct)
        print( String(data: jsonData, encoding: .utf8)! )
    } catch(let e) {
        print(e)
    }

/// 출력

{"variableNil":null,"variableSome":"test"}

// variablePending is missing, which is a correct behaviour

저도 같은 문제에 부딪혔어요.JSONEncoder를 사용하지 않고 구조에서 사전을 만들어 해결했습니다.이 작업은 비교적 보편적인 방법으로 수행할 수 있습니다.제 코드는 다음과 같습니다.

struct MyStruct: Codable {
    let id: String
    let regionsID: Int?
    let created: Int
    let modified: Int
    let removed: Int?


    enum CodingKeys: String, CodingKey, CaseIterable {
        case id = "id"
        case regionsID = "regions_id"
        case created = "created"
        case modified = "modified"
        case removed = "removed"
    }

    var jsonDictionary: [String : Any] {
        let mirror = Mirror(reflecting: self)
        var dic = [String: Any]()
        var counter = 0
        for (name, value) in mirror.children {
            let key = CodingKeys.allCases[counter]
            dic[key.stringValue] = value
            counter += 1
        }
        return dic
    }
}

extension Array where Element == MyStruct {
    func jsonArray() -> [[String: Any]] {
        var array = [[String:Any]]()
        for element in self {
            array.append(element.jsonDictionary)
        }
        return array
    }
}

이것은 CodingKeys 없이 실행할 수 있습니다(서버측의 테이블 속성명이 구조 속성명과 같은 경우).이 경우 mirror.children의 '이름'을 사용합니다.

CodingKeys가 필요한 경우 CaseItable 프로토콜을 추가하는 것을 잊지 마십시오.이를 통해 allCases 변수를 사용할 수 있습니다.

중첩된 구조에 주의하십시오. 예를 들어, 사용자 지정 구조를 유형으로 하는 속성이 있는 경우 해당 구조도 사전으로 변환해야 합니다.이것은 for 루프에서 실행할 수 있습니다.

MyStruct 사전 배열을 생성하려면 배열 확장이 필요합니다.

@Peterdk에서 설명한 바와 같이 이 문제에 대한 오류 보고서가 작성되었습니다.

https://bugs.swift.org/browse/SR-9232

향후 출시에서 이 기능이 어떻게 공식 API의 일부가 되어야 하는지를 고집하고 싶다면 언제든지 업 투표해 주십시오.

그리고 (Johan Nordberg에 의해) 이 버그리포트에 기재되어 있듯이, 각각을 고쳐 쓸 필요 없이 이 문제를 처리할 수 있는 라이브러리가 있습니다.encode(to:) 가능한 코드 가능한 구조 구현 ^^

에서는 이 줍니다.NULL의 값:

import Foundation
import FineJSON

extension URLRequest {

    init<T: APIRequest>(apiRequest: T, settings: APISettings) {

        // early return in case of main conf failure
        guard let finalUrl = URL(string: apiRequest.path, relativeTo: settings.baseURL) else {
            fatalError("Bad resourceName: \(apiRequest.path)")
        }

        // call designated init
        self.init(url: finalUrl)

        var parametersData: Data? = nil
        if let postParams = apiRequest.postParams {
            do {
                // old code using standard JSONSerializer :/
                // parametersData = try JSONSerializer.encode(postParams)

                // new code using FineJSON Encoder
                let encoder = FineJSONEncoder.init()

                // with custom 'optionalEncodingStrategy' ^^
                encoder.optionalEncodingStrategy = .explicitNull

                parametersData = try encoder.encode(postParams)

                // set post params
                self.httpBody = parametersData

            } catch {
                fatalError("Encoding Error: \(error)")
            }
        }

        // set http method
        self.httpMethod = apiRequest.httpMethod.rawValue

        // set http headers if needed
        if let httpHeaders = settings.httpHeaders {
            for (key, value) in httpHeaders {
                self.setValue(value, forHTTPHeaderField: key)
            }
        }
    }
}

이 문제에 대처하기 위해 필요한 변경은 이것뿐입니다.

훌륭한 lib를 해주신 Omochi씨 감사합니다.

도움이 됐으면 좋겠는데...

언급URL : https://stackoverflow.com/questions/47266862/encode-nil-value-as-null-with-jsonencoder

반응형