Skip to content

Use JSONSerialization instead of Yams for JSON #698

@marcelofabri

Description

@marcelofabri

Right now, JSON parsing is done via Yams.load_all since JSON is a subset of Yaml.

That's clever, but comes with a perfomance cost. For my project, I'm using SwiftGen to parse Lottie JSON animation files into enums and that's taking almost ~3s per (incremental build).

My initial thought was to add caching support (#695). But I think we can avoid that (for now?) and do something way simpler: using JSONSerialization instead. In my small test, this made swiftgen to take ~750ms instead.

This is what I did - I can clean this and submit a PR, just need some guidance:

// MARK: JSON File Parser

public enum JSON {
  public enum ParserError: Error, CustomStringConvertible {
    case invalidFile(path: Path, reason: String)

    public var description: String {
      switch self {
      case .invalidFile(let path, let reason):
        return "Unable to parse file at \(path). \(reason)"
      }
    }
  }

  public class Parser: SwiftGenKit.Parser {
    var files: [File] = []
    public var warningHandler: Parser.MessageHandler?

    public required init(options: [String: Any] = [:], warningHandler: Parser.MessageHandler? = nil) {
      self.warningHandler = warningHandler
    }

    public class var defaultFilter: String {
      return ".*\\.(?i:json)$"
    }

    public func parse(path: Path, relativeTo parent: Path) throws {
      files.append(try File(path: path, relativeTo: parent))
    }
  }

  // is it ok to duplicate this struct? I did it to avoid breaking any public APIs, but I guess I could extract it to another type and have a typealias here. Thoughts?
  struct File {
    let path: Path
    let name: String
    let documents: [Any]

    init(path: Path, relativeTo parent: Path? = nil) throws {
      guard let data: Data = try? path.read() else {
        throw ParserError.invalidFile(path: path, reason: "Unable to read file")
      }

      self.path = parent.flatMap { path.relative(to: $0) } ?? path
      self.name = path.lastComponentWithoutExtension

      do {
        let item = try JSONSerialization.jsonObject(with: data)
        self.documents = [item] // why does the Yaml version take an array? is this the right thing for JSON?
      } catch let error {
          throw ParserError.invalidFile(path: path, reason: error.localizedDescription)
      }
    }
  }
}


// this is pretty much the same as `Yaml.Parser`. What is the right abstraction here? Are we ok with duplicating it?
extension JSON.Parser {
  public func stencilContext() -> [String: Any] {
    let files = self.files
      .sorted { lhs, rhs in lhs.name < rhs.name }
      .map(map(file:))

    return [
      "files": files
    ]
  }

  private func map(file: JSON.File) -> [String: Any] {
    return [
      "name": file.name,
      "path": file.path.string,
      "documents": file.documents.map(map(document:))
    ]
  }

  private func map(document: Any) -> [String: Any] {
    return [
      "data": document,
      "metadata": Metadata.generate(for: document)
    ]
  }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions