Introduction
Why do we even need an SDK generator ?
Lets see an example of Stripe. They maintain SDKs across so many languages without constantly needing to sync updates. They do this by using SDK generators. When Stripe updates their API, they don’t manually rewrite code in seven different languages. Instead, they use code generation tools that read their API specification and automatically produce type-safe client libraries for each language.
Here is an example of their openapi specification
What is OpenAPI
OpenAPI is a global standard for describing REST APIs. The descriptions are made either in yaml or json format.
It contains information such as general description, metadata about the swagger version, and security information about how to access the API. It also lets us define paths (API endpoints) and the definitions (data structures for requests and responses).
One powerful feature is references. Instead of duplicating the same data structure across multiple endpoints, you define it once in the components/schemas section and reference it wherever needed.
Example: If you have a standard error response object used by all API endpoints, you can define it once as
#/components/schemas/ErrorResponseand reference it from every endpoint. When you need to update the error structure, you change it in one place and all references automatically reflect the updatesee
$refin this petstore.yaml ↗ example taken from swagger ↗.
in this project we have used this petstore example
A small sample of how an API specification loooks like:
openapi: 3.0.0
info:
title: Petstore API
description: A sample API for managing a pet store
version: 1.0.0
servers:
- url: https://petstore.swagger.io/v2
paths:
/pets:
get:
summary: List all pets
operationId: listPets
parameters:
- name: limit
in: query
schema:
type: integer
responses:
'200':
description: An array of pets
components:
schemas:
Pet:
type: object
required:
- name
properties:
id:
type: integer
description: Unique identifier
name:
type: string
description: Name of the pet
status:
type: string
enum: [available, pending, sold]openapi: 3.0.0
info:
title: Petstore API
description: A sample API for managing a pet store
version: 1.0.0
servers:
- url: https://petstore.swagger.io/v2
paths:
/pets:
get:
summary: List all pets
operationId: listPets
parameters:
- name: limit
in: query
schema:
type: integer
responses:
'200':
description: An array of pets
components:
schemas:
Pet:
type: object
required:
- name
properties:
id:
type: integer
description: Unique identifier
name:
type: string
description: Name of the pet
status:
type: string
enum: [available, pending, sold]Project Structure
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── bin
│ └── openapi-sdk-generator
├── examples
│ └── petstore_client.rb
├── lib
│ ├── openapi_sdk_generator
│ │ ├── generator.rb
│ │ ├── generators
│ │ │ ├── javascript_generator.rb
│ │ │ └── ruby_generator.rb
│ │ ├── parser.rb
│ │ ├── parsers
│ │ └── templates
│ │ ├── javascript_client.erb
│ │ ├── ruby_client.erb
│ │ └── ruby_model.erb
│ └── openapi_sdk_generator.rb
├── openapi_sdk_generator.gemspec
├── output
│ ├── ...
├── spec
│ └── parser_spec.rb
└── test
└── fixtures
└── petstore.yaml.
├── Gemfile
├── Gemfile.lock
├── README.md
├── bin
│ └── openapi-sdk-generator
├── examples
│ └── petstore_client.rb
├── lib
│ ├── openapi_sdk_generator
│ │ ├── generator.rb
│ │ ├── generators
│ │ │ ├── javascript_generator.rb
│ │ │ └── ruby_generator.rb
│ │ ├── parser.rb
│ │ ├── parsers
│ │ └── templates
│ │ ├── javascript_client.erb
│ │ ├── ruby_client.erb
│ │ └── ruby_model.erb
│ └── openapi_sdk_generator.rb
├── openapi_sdk_generator.gemspec
├── output
│ ├── ...
├── spec
│ └── parser_spec.rb
└── test
└── fixtures
└── petstore.yamlWorkflow Diagram
┌────────────────────────────┐
| OpenAPI Spec (YAML/JSON) |
└────────────┬───────────────┘
v
┌────────────┐
| Parser |
| parser.rb |
└────────────┘
Extracts: base_url, endpoints, models
|
v
┌───────────────────────┐
| Generator Base |
| generator.rb |
└────────────┬──────────┘
provides shared utilities
┌────────────────┴───────────────┐
v v
┌─────────────────────┐ ┌──────────────────────┐
| RubyGenerator | | JavascriptGenerator |
└─────────────────────┘ └──────────────────────┘
uses .erb templates uses .erb templates
( ruby_client.erb + ruby_model.erb ) ( javascript_client.erb )
| |
| |
| |
┌─────────────────────┐ ┌──────────────────┐
| Generated Ruby SDK | | Generated JS SDK |
└─────────────────────┘ └──────────────────┘ ┌────────────────────────────┐
| OpenAPI Spec (YAML/JSON) |
└────────────┬───────────────┘
v
┌────────────┐
| Parser |
| parser.rb |
└────────────┘
Extracts: base_url, endpoints, models
|
v
┌───────────────────────┐
| Generator Base |
| generator.rb |
└────────────┬──────────┘
provides shared utilities
┌────────────────┴───────────────┐
v v
┌─────────────────────┐ ┌──────────────────────┐
| RubyGenerator | | JavascriptGenerator |
└─────────────────────┘ └──────────────────────┘
uses .erb templates uses .erb templates
( ruby_client.erb + ruby_model.erb ) ( javascript_client.erb )
| |
| |
| |
┌─────────────────────┐ ┌──────────────────┐
| Generated Ruby SDK | | Generated JS SDK |
└─────────────────────┘ └──────────────────┘Step 1: Architecture
1. The Parser
The parser.rb ↗ is the brain of our generator. It reads OpenAPI specification file - petstore.yaml ↗
It has the ability to parse input both from a file and a url. Once it has all the content, it then Extracts API endpoints, parameters, response schemas and finally parses data models and their properties
module OpenapiSdkGenerator
class Parser
attr_reader :spec, :endpoints, :models, :base_url
def initialize(file_path)
@file_path = file_path
@spec = load_spec
@endpoints = []
@models = {}
@api_info = {}
parse_spec
end
...
private
def load_spec
content = fetch_content
parse_content(content)
rescue => e
raise Error, "Failed to load OpenAPI spec: #{e.message}"
end
def fetch_content
if url?(@file_path)
fetch_from_url(@file_path)
else
fetch_from_file(@file_path)
end
end
def fetch_content
if url?(@file_path)
fetch_from_url(@file_path)
else
fetch_from_file(@file_path)
end
end
def parse_spec
parse_info
parse_servers
parse_paths
parse_schemas
end
...
end
endmodule OpenapiSdkGenerator
class Parser
attr_reader :spec, :endpoints, :models, :base_url
def initialize(file_path)
@file_path = file_path
@spec = load_spec
@endpoints = []
@models = {}
@api_info = {}
parse_spec
end
...
private
def load_spec
content = fetch_content
parse_content(content)
rescue => e
raise Error, "Failed to load OpenAPI spec: #{e.message}"
end
def fetch_content
if url?(@file_path)
fetch_from_url(@file_path)
else
fetch_from_file(@file_path)
end
end
def fetch_content
if url?(@file_path)
fetch_from_url(@file_path)
else
fetch_from_file(@file_path)
end
end
def parse_spec
parse_info
parse_servers
parse_paths
parse_schemas
end
...
end
endparse_paths: Iterates through all API endpoints and extracts HTTP methods, paths, parameters, and responsesparse_schemas: Extracts data model definitions from thecomponents/schemassectionparse_parameters: Handles query, path, header, and body parameters
Here is how it looks
2. Language-Specific Generators
All of our language-specific generators i.e ruby_generator.rb and javascript_generator.eb will inherit from generator.rb ↗. It provides common utility methods and enforces rules that all its subclasses must follow. eg, they must implement methods def generate and def write_to_directory
Ruby Generator - ruby_generator.rb ↗
This class is responsible for generating ruby files as follows:
├── output
│ ├── README.md
│ ├── client.rb
│ └── models
│ ├── error.rb
│ ├── newpet.rb
│ └── pet.rb
├── output
│ ├── README.md
│ ├── client.rb
│ └── models
│ ├── error.rb
│ ├── newpet.rb
│ └── pet.rb
module OpenapiSdkGenerator
module Generators
class RubyGenerator < Generator
...
def write_to_directory(output_dir)
FileUtils.mkdir_p(output_dir)
FileUtils.mkdir_p(File.join(output_dir, 'models'))
# Generate client.rb
client_content = generate_client
File.write(File.join(output_dir, 'client.rb'), client_content)
# Generate models
parser.models.each do |name, model|
model_content = generate_model(model)
filename = "#{sanitize_name(name)}.rb"
File.write(File.join(output_dir, 'models', filename), model_content)
end
# Write readme file
readme_content = generate_readme
File.write(File.join(output_dir, 'README.md'), readme_content)
end
...
end
end
endmodule OpenapiSdkGenerator
module Generators
class RubyGenerator < Generator
...
def write_to_directory(output_dir)
FileUtils.mkdir_p(output_dir)
FileUtils.mkdir_p(File.join(output_dir, 'models'))
# Generate client.rb
client_content = generate_client
File.write(File.join(output_dir, 'client.rb'), client_content)
# Generate models
parser.models.each do |name, model|
model_content = generate_model(model)
filename = "#{sanitize_name(name)}.rb"
File.write(File.join(output_dir, 'models', filename), model_content)
end
# Write readme file
readme_content = generate_readme
File.write(File.join(output_dir, 'README.md'), readme_content)
end
...
end
end
endHere we can see the output that is going to get written in these files
JavaScript Generator - javascript_generator.rb ↗
This class is responsible for generating javascript files as follows:
├── output
│ ├── README.md
│ ├── client.js
│ └── package.json
├── output
│ ├── README.md
│ ├── client.js
│ └── package.json
module OpenapiSdkGenerator
module Generators
class JavascriptGenerator < Generator
...
def write_to_directory(output_dir)
FileUtils.mkdir_p(output_dir)
# Write client file
client_content = generate_client
File.write(File.join(output_dir, 'client.js'), client_content)
# Write package.json
package_json = generate_package_json
File.write(File.join(output_dir, 'package.json'), package_json)
# Write readme file
readme_content = generate_readme
File.write(File.join(output_dir, 'README.md'), readme_content)
end
...
end
end
endmodule OpenapiSdkGenerator
module Generators
class JavascriptGenerator < Generator
...
def write_to_directory(output_dir)
FileUtils.mkdir_p(output_dir)
# Write client file
client_content = generate_client
File.write(File.join(output_dir, 'client.js'), client_content)
# Write package.json
package_json = generate_package_json
File.write(File.join(output_dir, 'package.json'), package_json)
# Write readme file
readme_content = generate_readme
File.write(File.join(output_dir, 'README.md'), readme_content)
end
...
end
end
endHere is how the output looks that is going to get written into these files
4. Templates
we have prewritten templates that get dynamically populated during the runtime. These templates are used by our ruby and javascript generators
For example Lets see how this ruby_model.erb ↗ generates output
# <%= @current_model[:name] %>
class <%= camelize(@current_model[:name]) %>
<%- @current_model[:properties].each do |name, prop| -%>
attr_accessor :<%= sanitize_name(name) %>
<%- end -%>
def initialize(attributes = {})
<%- @current_model[:properties].each do |name, prop| -%>
@<%= sanitize_name(name) %> = attributes['<%= name %>'] || attributes[:<%= sanitize_name(name) %>]
<%- end -%>
end
def to_h
{
<%- @current_model[:properties].keys.each_with_index do |name, index| -%>
'<%= name %>' => @<%= sanitize_name(name) %><%= index < @current_model[:properties].keys.length - 1 ? ',' : '' %>
<%- end -%>
}
end
def to_json(*args)
to_h.to_json(*args)
end
def self.from_json(json)
data = JSON.parse(json)
new(data)
end
end# <%= @current_model[:name] %>
class <%= camelize(@current_model[:name]) %>
<%- @current_model[:properties].each do |name, prop| -%>
attr_accessor :<%= sanitize_name(name) %>
<%- end -%>
def initialize(attributes = {})
<%- @current_model[:properties].each do |name, prop| -%>
@<%= sanitize_name(name) %> = attributes['<%= name %>'] || attributes[:<%= sanitize_name(name) %>]
<%- end -%>
end
def to_h
{
<%- @current_model[:properties].keys.each_with_index do |name, index| -%>
'<%= name %>' => @<%= sanitize_name(name) %><%= index < @current_model[:properties].keys.length - 1 ? ',' : '' %>
<%- end -%>
}
end
def to_json(*args)
to_h.to_json(*args)
end
def self.from_json(json)
data = JSON.parse(json)
new(data)
end
endThe output generated file:
# Pet
class Pet
attr_accessor :name
attr_accessor :tag
attr_accessor :id
def initialize(attributes = {})
@name = attributes['name'] || attributes[:name]
@tag = attributes['tag'] || attributes[:tag]
@id = attributes['id'] || attributes[:id]
end
def to_h
{
'name' => @name,
'tag' => @tag,
'id' => @id
}
end
def to_json(*args)
to_h.to_json(*args)
end
def self.from_json(json)
data = JSON.parse(json)
new(data)
end
end# Pet
class Pet
attr_accessor :name
attr_accessor :tag
attr_accessor :id
def initialize(attributes = {})
@name = attributes['name'] || attributes[:name]
@tag = attributes['tag'] || attributes[:tag]
@id = attributes['id'] || attributes[:id]
end
def to_h
{
'name' => @name,
'tag' => @tag,
'id' => @id
}
end
def to_json(*args)
to_h.to_json(*args)
end
def self.from_json(json)
data = JSON.parse(json)
new(data)
end
endWe other templates we have used are these:
Step 2: Running the application
There are 2 ways to generate output. we have these 2 script files.
-
openapi-sdk-generator ↗ acts as the “front desk” of our SDK Generator. It collects user input via command-line flags and validates them. If everything is fine, it passes control to the generator which handles ruby and js file generation.
-
petstore_client.rb ↗ - This is a cutom script written only for our petstore example for our ease
Example - Using File:
./bin/openapi-sdk-generator --help
./bin/openapi-sdk-generator -i test/fixtures/petstore.yaml -o ./output -l javascript # generates only js files
./bin/openapi-sdk-generator -i test/fixtures/petstore.yaml -o ./output -l ruby # generates only ruby files
ruby examples/petstore_client.rb # generates both files ./bin/openapi-sdk-generator --help
./bin/openapi-sdk-generator -i test/fixtures/petstore.yaml -o ./output -l javascript # generates only js files
./bin/openapi-sdk-generator -i test/fixtures/petstore.yaml -o ./output -l ruby # generates only ruby files
ruby examples/petstore_client.rb # generates both files Here is how you can use it.
Example - Using Url
./bin/openapi-sdk-generator -i https://raw.githubusercontent.com/taimourz/openapi_sdk_generator_gem/refs/heads/main/test/fixtures/petstore.yaml -o ./output -l ruby
./bin/openapi-sdk-generator -i https://raw.githubusercontent.com/taimourz/openapi_sdk_generator_gem/refs/heads/main/test/fixtures/petstore.yaml -o ./output -l javascript./bin/openapi-sdk-generator -i https://raw.githubusercontent.com/taimourz/openapi_sdk_generator_gem/refs/heads/main/test/fixtures/petstore.yaml -o ./output -l ruby
./bin/openapi-sdk-generator -i https://raw.githubusercontent.com/taimourz/openapi_sdk_generator_gem/refs/heads/main/test/fixtures/petstore.yaml -o ./output -l javascriptStep 3: Testing with RSpec
Testing is extemely crucial to verify if the genrated files are correct. In order to test our generated files we have written one spec file: parser_spec.rb ↗. Right now, we only have tests for ruby but they are enough to get the point across.
This spec ensures that:
- The OpenAPI document loads successfully
- API metadata (title, version, description) is correctly parsed
- Endpoints and parameters are extracted with correct HTTP methods & paths
- Models, properties, and required fields are resolved

Step 4: Publishing/Updating Gem
1. Building
Our openapi_sdk_generator.gemspec ↗ defines the gem metadata.
Sign up on ruby gems org and generate an API key
mkdir -p ~/.gem
vim ~/.gem/credentialsmkdir -p ~/.gem
vim ~/.gem/credentialscopy paste your api key like this ( Dont worry api key is incorrect 😎 )
---
:rubygems_api_key: rubygems_7bb74d7bb74d3f6468fdasfdasdafdsafdsa16c8b92dadfasda6adfd---
:rubygems_api_key: rubygems_7bb74d7bb74d3f6468fdasfdasdafdsafdsa16c8b92dadfasda6adfdFinally build and push
chmod 0600 ~/.gem/credentials
gem signin
gem build openapi_sdk_generator.gemspec
gem push openapi_sdk_generator_gem-0.1.0.gemchmod 0600 ~/.gem/credentials
gem signin
gem build openapi_sdk_generator.gemspec
gem push openapi_sdk_generator_gem-0.1.0.gemHere is how i did it
![]()
2. Pull from Ruby.org and verify
gem install openapi_sdk_generator_gem
ls ~/.rvm/gems/ruby-3.4.5/gems/openapi_sdk_generator_gem-0.1.0 # look in your own ruby version
cat ~/.rvm/gems/ruby-3.4.5/gems/openapi_sdk_generator_gem-0.1.0/bin/openapi-sdk-generatorgem install openapi_sdk_generator_gem
ls ~/.rvm/gems/ruby-3.4.5/gems/openapi_sdk_generator_gem-0.1.0 # look in your own ruby version
cat ~/.rvm/gems/ruby-3.4.5/gems/openapi_sdk_generator_gem-0.1.0/bin/openapi-sdk-generator
That’s it! Your gem is now live on RubyGems.org and anyone can install it.
3. Updating
Now that we have successfully published our gem and when we need to make an update, we would want to update it at regular intervals. Here is how we do it
First we need to increase the version number like this:

Finally push the updated gem:
gem build openapi_sdk_generator.gemspec
gem push openapi_sdk_generator_gem-0.1.1.gemgem build openapi_sdk_generator.gemspec
gem push openapi_sdk_generator_gem-0.1.1.gem



