I had a lot of fun listening to “A releational database using Markdown” episode with John Sundell and Gui Rambo:

After taking a moment though, I started realizing that there is a whole plethora of use cases where Markdown could be a great storage for some of my data. Markdown supports tables and they are pretty easy to view, parse and edit by hand.

On the other hand, JSON isn’t very easy to view or edit by hand, which is sometimes more important for me, given I work on a lot of small tooling that automates or performs specific tasks and usually doesn’t need a large dataset. But it’s often important to be able to view and edit the data, if needed.

And so, this is how the idea for MarkCodable came to be.

A Markdown Codable

My first thought was to define a semantic markdown spec that could express any kind of nested Codable structure, much like what JSON does.

But… I do have some similar experience with helping folks to use Swift DocC’s custom Markdown format. Documentation Markdown has just a couple of features on top of the common Markdown format but that’s still a cliff many won’t climb on their own — it’s non-trivial to do that.

So, I decided to focus on the most straight-forward, common use case of encoding simpler, less complex data in a Markdown table. Then, if that picks up, think about a more complex format to handle more advanced use cases.

Encoding a single value

Long story short, MarkCodable flattens your Codable structures and plots them into a plain text table as a string. First, you define a Codable structure or class like usual, for example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct House: Codable {
    var isSocial = true
    var street: String
    var number: Int
    var price: Price
    
    struct Price: Codable {
        var price: Double
        var currency: String
        var conversionRate: Float?
    }
}

Then you create some instances and encode them as Markdown:

1
2
3
4
5
6
7
8
9
let house = House(
    isSocial: true,
    street: "Main St",
    number: 239,
    price: House.Price(price: 20_000, currency: "EUR")
)

let markdown = try MarkEncoder().encode(house)
print(markdown)

And the output you get is valid, deterministic Markdown:

|isSocial|number|price.currency|price.price|street |
|--------|------|--------------|-----------|-------|
|true    |239   |EUR           |20000.0    |Main St|

When you open this in a Markdown viewer you get a proper table:

markdown table preview in Marked2 app

Encoding collections

Later on, you can decode the markdown (from a file or memory), modify the data and encode it back as text. For example, let’s decode the markdown that we had so far as an array of houses:

1
var houses = try MarkDecoder().decode([House].self, string: markdown)

Then add one more house to the collection:

1
2
3
4
5
6
7
let house2 = House(
    isSocial: false,
    street: "Market St",
    number: 84,
    price: House.Price(price: 230_000, currency: "JPY")
)
houses.append(house2)

And finally, encode it again and see the resulting Markdown:

1
2
let newMarkdown = try MarkEncoder().encode(houses)
print(newMarkdown)

This time you get:

|isSocial|number|price.currency|price.price|street   |
|--------|------|--------------|-----------|---------|
|true    |239   |EUR           |20000.0    |Main St  |
|false   |84    |JPY           |230000.0   |Market St|

Simple, isn’t it?

Interoperability with JSONEncoder

One of the benefits of using a Codable instead of a custom Markdown writer is that you can use MarkEncoder for human friendly output but still fall back on JSON with JSONEncoder if you want to send the data to a web API:

1
2
3
let data = try JSONEncoder().encode(houses)
let json = String(decoding: data, as: UTF8.self)
print(json)
[
  {
    "number": 239,
    "street": "Main St",
    "isSocial": true,
    "price": {
      "price": 20000,
      "currency": "EUR"
    }
  },
  {
    "number": 84,
    "street": "Market St",
    "isSocial": false,
    "price": {
      "price": 230000,
      "currency": "JPY"
    }
  }
]

Working with more data

While, the primary focus of MarkCodable is to offer humans a trivial plain-text data format, should your markdown grow in size or complexity you can always use a more powerful tool to browse or edit the data. Below, I’ve opened my Markdown file with the TableFlip app to easily add more houses to my listing in a GUI:

Markdown codable use cases

If you already think this is something that might be useful for you, head straight to https://github.com/icanzilb/MarkCodable.

Otherwise, here are just few use cases that might resonate with you if you had to do something similar in the past.

Configuration files

Assume you have an easily editable config file config.md:

| environment | host      | port | user | schema |
|-------------|-----------|------|------|--------|
| qa          | 127.0.0.1 | 8080 | test | http   |
| production  | 2.317.1.2 | 9999 | app  | https  |

Use MarkDecoder to get the config you need for a particular run of your app:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Config: Codable {
    let environment: String
    let host: String
    let port: UInt
    let user: String
}

let markdown = try String(contentsOfFile: "config.md")

let configs = try MarkDecoder()
    .decode([Config].self, string: markdown)
    .reduce(into: [String: Config](), { $0[$1.environment] = $1 })

print(configs["qa"])

Unit tests data objects

Sometimes you need some extra data to create the system under test in your unit tests. Often it’s too noisy to create all the objects in code or non-trivial if the data structures are still in flux.

You can easily embed and edit Markdown in your Swift code and keep your tests clean. Let’s explore an example — first let’s define a struct called Computer that we want to test:

1
2
3
4
struct Computer: Codable {
    let a, b: Int
    func sum() -> Int { a + b }
}

Now we can create as many instances as we want by decoding a simple markdown string in the unit test like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func testComputer() throws {
    let inputs = """
    | a | b |
    |-|-|
    | 0 | 0 |
    | 7 | 2 |
    | 2 | 4 |
    | 1 | 1 |
    """
    let expected = [0, 9, 6, 2]

    let results = try MarkDecoder()
        .decode([Computer].self, string: inputs)
        .map { $0.sum() }

    XCTAssertEqual(expected, results)
}

Simple Database

Finally, I think using Markdown as a simple database format is a great choice. You can edit the data in any text editor locally or online on GitHub.

Having your data in plain text format gives you change history via Git, change approvals via GitHub PRs, hosting and authentication, if needed, via GitHub or GitLab.

In fact, I went ahead and put together a simple Markdown-driven SwiftUI app of about 70 lines of code that:

  • 🤖 uses GitHub for backend
  • 🖼 SwiftUI + MarkCodable for the iPhone app
  • 💵 Amazon for payments

Check out the code here: https://github.com/icanzilb/MarkCodingDemoApp

Where to go from here?

I wrote a bit more about the motivation behind MarkCodable here:

Let me know what you think!

Thanks for reading! If you’d like to support me, get my book on Swift Concurrency:

» swiftconcurrencybook.com «

In case you want to talk more, hit me up on twitter at https://twitter.com/icanzilb.