0

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Zera Zinc
  • 29
  • 4
  • You can't do this directly. However, you can create a custom subclass of the encoders and decoders with `super.init()` etc... and then use those in your app. –  Apr 30 '23 at 23:31
  • 1
    JSONDecoder has no subclassing notes, so you should assume it is not designed to be subclassed. Instead, you would use an extension that returns a desired configured object. As a rule, be extremely careful (and avoid if possible) subclassing types that are not explicitly designed for subclassing. – Rob Napier May 01 '23 at 02:15
  • 1
    Just a side note on why this isn't possible (the answers below gives lots of other ways to approach the problem). Imagine it *were* possible to change the global settings for all JSONDecoders. This would impact any Apple and third-party frameworks that you import that use JSON internally, breaking their serializations. You really don't want this to be global. You want it to be easy to access within your system. So you should create an extension that makes it easy to configure it the way you want, but not **globally**. – Rob Napier May 01 '23 at 13:38
  • @RobNapier - “has no subclassing notes, so you should assume it is not designed to be subclassed” … That’s an interesting take. I would have assumed the exact opposite, that all classes are subclass’able unless the documentation says otherwise. A “class” means, almost by definition, that it can be subclassed. And generally where you cannot subclass, or it is ill-advised, the docs are good about warning us. That having been said, subclassing is just the wrong pattern here (subclasses should extend behaviors, not change them). – Rob May 01 '23 at 17:20

4 Answers4

2

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

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)
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Is it safe for an app to rely on a single instance of a `JSON[En|De]coder` for the lifetime of the app? Are they thread safe and/or reusable? – HangarRash May 01 '23 at 15:18
  • 1
    @HangarRash I suppose. `Codable` uses `JSONserialization` under the hood – which is thread-safe – and according to the [source code](https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/JSONDecoder.swift) there is no shared storage. – vadian May 01 '23 at 15:39
  • Thanks for the link. I guess the only possible issue would be if different parts of the app started setting different values to other properties on the `iso8601` instance. That would make for some interesting bugs to track down. – HangarRash May 01 '23 at 16:06
  • @HangarRash - One should never assume that a reference type is thread safe (unless it is `Sendable` or the documentation otherwise makes some formal assurances to this effect). Besides, you have no assurances that some call point couldn’t mutate one or more properties of the referenced object. I would strongly advise against the `static` pattern. – Rob May 01 '23 at 16:21
0

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.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
0

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
Rob
  • 415,655
  • 72
  • 787
  • 1,044