Written by James Britt
To combine Ruby and Swift, start with a shared data format. In this tutorial, Ruby writes a small JSON file and Swift reads it into typed structures. You get two ordinary programs with a clear hand-off, which makes the combined Ruby Swift programming tutorial code easier to run, inspect and change.
This is a local command-line exercise. It does not embed Ruby inside an iOS application or make Swift execute Ruby source. Once the file exchange works, the same data contract can inform a later API. Authentication, network failures and application packaging would be separate work.

What the Ruby and Swift programs will share
Our example exports a short reading list. Each item has a text name and a whole-number quantity. The top-level object also has a schema version. That version describes the file format agreed by these two programs; it is unrelated to the installed Ruby or Swift version.
The contract accepts at most 1,000 items, with quantities from 0 to 10,000. Those modest limits keep the demonstration’s total within a comfortable range. The consumer checks them after decoding. A valid JSON document is not automatically valid application data.
Use a fresh working folder with Ruby and a Swift toolchain installed. Confirm that ruby --version and swift --version work in your terminal. The tutorial uses Foundation in Swift and Ruby’s JSON library. It needs no Rails app, package dependency or running web server.
Step 1: write the Ruby exporter
Save the following as export.rb. It writes catalog.json in the current working directory, replacing a file with that name if one already exists. Use the fresh tutorial folder so the output has a clear owner.
require "json"
catalog = {
"schema_version" => 1,
"items" => [
{ "name" => "Ruby guide", "quantity" => 2 },
{ "name" => "Swift notes", "quantity" => 1 }
]
}
File.write("catalog.json", JSON.generate(catalog) + "\n")
puts "Wrote catalog.json"
Ruby’s JSON reference documents the conversion of hashes and arrays with JSON.generate. The result is a string containing JSON. File.write then writes that string to disk.
Do not replace the generator with catalog.to_s. A Ruby object’s diagnostic representation is not the data format promised to the Swift reader. A serializer also handles escaping characters inside strings, so you should not assemble JSON by joining fragments of user text.
Run ruby export.rb. After the confirmation message, open the generated file in a text editor. You should see the two named items and a numeric schema version. The file may be compact; whitespace is not what makes the data valid.
Step 2: write the Swift importer
Save this code as import.swift in the same folder. The CodingKeys enumeration maps Ruby’s JSON key schema_version to the Swift property schemaVersion. The two names can differ because the mapping is explicit.
import Foundation
struct Item: Decodable {
let name: String
let quantity: Int
}
struct Catalog: Decodable {
let schemaVersion: Int
let items: [Item]
enum CodingKeys: String, CodingKey {
case schemaVersion = "schema_version"
case items
}
}
enum CatalogError: Error {
case unsupportedVersion
case tooManyItems
case invalidItem
}
func readCatalog(from path: String) throws -> Catalog {
let data = try Data(contentsOf: URL(fileURLWithPath: path))
let catalog = try JSONDecoder().decode(Catalog.self, from: data)
guard catalog.schemaVersion == 1 else {
throw CatalogError.unsupportedVersion
}
guard catalog.items.count <= 1_000 else {
throw CatalogError.tooManyItems
}
guard catalog.items.allSatisfy({ item in
!item.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& (0...10_000).contains(item.quantity)
}) else {
throw CatalogError.invalidItem
}
return catalog
}
let path = CommandLine.arguments.dropFirst().first ?? "catalog.json"
do {
let catalog = try readCatalog(from: path)
for item in catalog.items {
print("\(item.name): \(item.quantity)")
}
let total = catalog.items.reduce(0) { $0 + $1.quantity }
print("Total copies: \(total)")
} catch {
FileHandle.standardError.write(Data("Import failed: \(error)\n".utf8))
exit(1)
}
Apple’s guide to encoding and decoding custom types explains the protocol-based approach. This program only reads data, so its structures adopt Decodable. You would use Codable when the same type needs both encoding and decoding support.
First, the decoder checks the required fields and their types. The guards then check application rules: a supported schema, a bounded list, a non-blank name and an allowed quantity. These are separate stages. For example, a negative integer can decode successfully but fail the quantity rule.
The error handler reports failures in one place and exits with a nonzero status. That lets a shell or a later automation job tell success from failure. The diagnostic is useful for this exercise; a user-facing application would usually translate it into a clearer message.
Step 3: run the combined Ruby Swift programming tutorial code
The examples passed end-to-end checks with Ruby 3.2.3 and Swift 6.2.1 on Linux, including the compiled and script forms of the importer. From the folder containing both source files, run these commands in order. The first creates the input consumed by the second.
ruby export.rb
swift import.swift catalog.json
The exporter prints Wrote catalog.json. The importer should then print:
Ruby guide: 2
Swift notes: 1
Total copies: 3
Run the importer again without changing catalog.json. It should produce exactly the same result. That repeatability helps with debugging: a fixed file lets you test the consumer independently of the producer.
If you prefer a compiled executable, run swiftc import.swift -o catalog-import, followed by ./catalog-import catalog.json on macOS or Linux. Both the interpreted command and the compiled program use the same source and contract.
Break the contract and diagnose the result
Change a copy of catalog.json so that it breaks one part of the contract, then pass that file’s path to the importer. Predict which stage will reject it before opening the answer.
Change a quantity from 2 to “2”
The value has become JSON text instead of a number. Decoding into Int fails. Keep the producer and consumer agreement explicit; do not silently convert every unexpected type just to make the import continue.
Change schema_version from 1 to 2
The data can decode, but the version guard rejects it. A future format needs a deliberate migration or a consumer that understands both versions. Changing the number alone does not add compatibility.
Remove the file or make an item name blank
A missing file fails during the read. A blank name reaches the application validation and fails there. In both cases the program reports an import failure and returns a nonzero exit status.
Extend the example without hiding its boundaries
Try an empty items array next. It is valid under this contract and produces Total copies: 0. By contrast, a missing items key is an error. An empty collection and absent required data are different states.
For more Ruby practice, use our Ruby code examples. If the exporter needs to filter or transform a collection, our functional programming guide can help you keep that work understandable.
This small reader loads the whole file into memory. Its item limit applies after decoding, so it is not a pre-read file-size limit. For large or untrusted files, add appropriate size controls and consider a different ingestion design. When moving to HTTP, also define timeouts, authentication and retry behaviour.
Keep JSON as data. Neither program needs eval, executable content or a shared runtime. Sample files, a written schema and tests for rejected inputs make the agreement between the two languages easier to maintain.
