How can I apply a global .dateDecodingStrategy for all JSONEncoder and JSONDecoder?

249 Views Asked by At

I haven't found any info/similar questions here. I need to use .iso8601 for my JSONDecoder and JSONEncoder. Current I am doing this for each HTTP call:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
URLSession.shared.dataTaskPublisher(for: request)
    .receive(on: DispatchQueue.main)
    .tryMap(handleHTTPOutput)
    .decode(type: [TaskModel].self, decoder: decoder)
    ...

Is there any way to change the default value of dateDecodingStrategy for all the JSONEncoder and JSONDecoder?

I was trying to do the below but it doesn't work:

JSONDecoder.DateDecodingStrategy = iso8601

The error says the DateDecodingStrategy is immutable.

4

There are 4 best solutions below

0
Daniel T. On

No matter what you do, you will have to go to every spot where you are constructing a JSONEncoder/Decoder and change it. Just change them all to use a single global Encoder/Decoder.

You can store the global in an extension to avoid name pollution:

extension JSONEncoder {
    static let shared: JSONEncoder = {
        var res = JSONEncoder()
        res.dateEncodingStrategy = .iso8601
        return res
    }()
}

extension JSONDecoder {
    static let shared: JSONDecoder = {
        var res = JSONDecoder()
        res.dateDecodingStrategy = .iso8601
        return res
    }()
}

Now do a global find/replace from JSONDecoder(). to JSONDecoder.shared. and from JSONEncoder(). to JSONEncoder.shared.

4
vadian On

You cannot change the default value of the key/date/data strategies but you can extend both types and add static properties with the desired behavior.

For example define an iso8601 En-/Decoder as well as a snakeCase En-/Decoder

extension JSONEncoder {
    static let iso8601 : JSONEncoder = {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return encoder
    }()
    
    static let snakeCase : JSONEncoder = {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        return encoder
    }()
}

extension JSONDecoder {
    static let iso8601 : JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return decoder
    }()
    
    static let snakeCase : JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }()
}

The benefit is you can write

.decode(type: [TaskModel].self, decoder: JSONDecoder.iso8601)
1
AudioBubble On

Other answers provide the syntax for using this outside of call site. No one has mentioned to use static member lookup for you there yet, though. That looks like this:

extension TopLevelDecoder where Self == JSONDecoder {
  static var json: Self {
    let `self` = JSONDecoder()
    self.dateDecodingStrategy = .iso8601
    return self
  }
}
.decode(type: [TaskModel].self, decoder: .json)

That's all you need if you're only ever using that decoder in decode functions.


But you probably aren't only doing that, for encoding. You can provide two static variables: one on the type, and one on the constrained protocol, to handle both use cases:

extension JSONEncoder {
  static var iso8601: JSONEncoder {
    let `self` = JSONEncoder()
    self.dateEncodingStrategy = .iso8601
    return self
  }
}

extension TopLevelEncoder where Self == JSONEncoder {
  static var json: Self { .iso8601 }
}
try JSONEncoder.iso8601.encode(TaskModel())
… // Publisher<[TaskModel], _>
.encode(encoder: .json)
0
Rob On

I would advise against subclass approach. Subclasses are best suited when extending a class with new behaviors, not when changing its behaviors.

There are a couple of approaches I would consider.

  1. One is to just have a convenience initializer that takes the date decoding strategy:

    extension JSONDecoder { 
        /// Return new `JSONDecoder` with particular date decoding strategy
        convenience init(dateDecodingStrategy: JSONDecoder.DateDecodingStrategy) {
            self.init()
            self.dateDecodingStrategy = dateDecodingStrategy
        }
    }
    

    Then you can do things like:

    URLSession.shared.dataTaskPublisher(for: request)
        .receive(on: DispatchQueue.main)
        .tryMap(handleHTTPOutput)
        .decode(type: [TaskModel].self, decoder: JSONDecoder(dateDecodingStrategy: .iso8601))
    
  2. Another approach is to create your own factory method with the appropriate date decoding strategy:

    extension TopLevelDecoder where Self == JSONDecoder { 
        /// Return new `JSONDecoder` with `.iso8601` date decoding strategy
        static func jsonWithIso8601() -> JSONDecoder {
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .iso8601
            return decoder
        }
    }
    

    Then you can do things like:

    URLSession.shared.dataTaskPublisher(for: request)
        .receive(on: DispatchQueue.main)
        .tryMap(handleHTTPOutput)
        .decode(type: [TaskModel].self, decoder: .jsonWithIso8601())
    
  3. I generally have multiple properties I have to set up for my encoders/decoders, so I personally have a private (or internal) factory for decoders for my particular web service:

    private extension TopLevelDecoder where Self == JSONDecoder { 
        /// Return new `JSONDecoder` designed for the “Foobar” web service
        static func forFoobar() -> JSONDecoder {
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .iso8601
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            …
            return decoder
        }
    }
    

    Then you can do things like:

    URLSession.shared.dataTaskPublisher(for: request)
        .receive(on: DispatchQueue.main)
        .tryMap(handleHTTPOutput)
        .decode(type: [TaskModel].self, decoder: .forFoobar())
    

There are lots of variations on the theme, but hopefully this illustrates a few basic ideas.

But, as a general design principle, I would generally advise against a static property given that this is a reference type with its own mutable properties. You might be careful to not mutate it right now, but it is the sort of thing that bites you a year or two later when accidentally mutate one of the properties, unaware of the unintended consequences that sharing introduced.


Regarding your error trying to set DateDecodingStrategy, that is not a property. It is an enum (for example, as used as a parameter type in the convenience initializer of my first example, above).

It would be analogous to:

enum Answer {
    case yes
    case no
}

var answer: Answer = .yes   // fine
answer = .no                // fine

Answer = .no                // ERROR: this makes no sense; `Answer` is a type, an `enum`, not a property or variable