Tickd.ai
← The Tickd Guide

Tutorials & Guides

How to Build a Local Markdown-to-API Spec Converter Using Gemini 1.5 Flash and Python

Stop writing tedious OpenAPI YAML by hand. Learn how to parse raw markdown documentation into valid, production-ready OpenAPI 3.0.0 specifications using Gemini 1.5 Flash and Python structured outputs.

Updated 10/15/2026

Writing OpenAPI (Swagger) specifications is one of those development tasks that drains the soul. It is tedious, incredibly pedantic about indentation, and frankly, a waste of your cognitive load. Developers are great at writing quick markdown notes about how their endpoints work, but terrible at maintaining the massive JSON or YAML files required to keep modern API gateways and mock servers happy.

We can solve this permanently by building a lightweight, local CLI tool that takes a messy markdown file and converts it into a perfectly formatted, schema-validated OpenAPI 3.0.0 spec. To do this, we are using Gemini 1.5 Flash. Why Gemini? Because its native support for structured JSON schemas (via Pydantic) is rock-solid, its context window is massive, and it is fast enough to run in a Git hook without making you grow a beard while waiting.

Let’s build a clean, local Python utility to automate the boring stuff.

Prerequisites

Before we write any code, make sure you have the following installed:

  • Python 3.10 or higher
  • A Google AI Studio API key (you can grab one from their developer portal)

First, set up your project directory and install the required dependencies:

`bash mkdir md-to-api && cd md-to-api python3 -m venv venv source venv/bin/activate pip install google-generativeai pydantic `

Set your API key as an environment variable in your terminal:

`bash export GEMINI_API_KEY="your_api_key_here" `

Step 1: Define the Output Schema with Pydantic

To make sure the LLM doesn’t hallucinate random field names or output conversational fluff, we will enforce a strict JSON output schema. Gemini supports Pydantic models directly, allowing us to define the precise structure of an OpenAPI object.

Create a file named converter.py and set up your imports and schema classes:

`python import os import sys import json import google.generativeai as genai from pydantic import BaseModel, Field from typing import List, Dict, Optional

Define the target OpenAPI schema structures class ParameterSchema(BaseModel): name: str = Field(description="The name of the parameter") in_: str = Field(alias="in", description="The location of the parameter (path, query, header, cookie)") required: bool type: str = Field(description="Data type (string, integer, boolean, etc.)") description: Optional[str] = None

class ResponseSchema(BaseModel): status_code: str = Field(description="HTTP status code as a string, e.g., '200' or '400'") description: str schema_example: Optional[str] = Field(None, description="Valid JSON representation of the response body, stringified")

class EndpointSchema(BaseModel): path: str = Field(description="The relative path URL, e.g., /users/{id}") method: str = Field(description="HTTP method in lowercase, e.g., get, post, put, delete") summary: str description: Optional[str] = None parameters: List[ParameterSchema] = [] request_body_example: Optional[str] = Field(None, description="Valid JSON representation of the request body, stringified") responses: List[ResponseSchema]

class OpenApiSpec(BaseModel): openapi: str = "3.0.0" info_title: str = Field(description="The title of the API") info_version: str = Field(description="The version of the API, e.g., 1.0.0") info_description: Optional[str] = None endpoints: List[EndpointSchema] `

By feeding these Pydantic models straight to Gemini, we ensure we get predictable, structured data back every single run. It's the secret to making LLM utility tools feel robust rather than experimental.

Step 2: Write the Prompt and Gemini Integration

Next, we need to instruct Gemini 1.5 Flash on how to map our informal markdown structures (like loose markdown tables and scratchpad descriptions) into our rigid API schema. We can direct Gemini to extract paths, methods, request schemas, and responses accurately.

If you want to sharpen your overall prompting skills before continuing, take a look at our interactive prompt builder to master structuring complex developer outputs.

Add the generation logic to converter.py:

`python def convert_markdown_to_spec(markdown_content: str) -> str: # Initialise the Gemini API client api_key = os.environ.get("GEMINI_API_KEY") if not api_key: print("Error: GEMINI_API_KEY environment variable not set.", file=sys.stderr) sys.exit(1)

genai.configure(api_key=api_key)

System instructions to lock down behavior system_instruction = ( "You are an expert backend engineer and OpenAPI architect. Your task is to ingest raw, informal " "markdown API documentation and map it precisely to the provided OpenApiSpec JSON structure. " "Ensure all endpoints, parameter types, HTTP methods, and response payloads are parsed correctly. " "Do not make up missing details unless they are logical defaults (like assuming status code '200' for success). " "All JSON examples inside request/response fields must be valid JSON strings, not raw unescaped JSON objects." )

model = genai.GenerativeModel( model_name="gemini-1.5-flash", system_instruction=system_instruction )

Instructing the model to use our Pydantic model for structured outputs response = model.generate_content( f"Please parse the following markdown documentation into the required OpenAPI schema:\n\n{markdown_content}", generation_config=genai.types.GenerationConfig( response_mime_type="application/json", response_schema=OpenApiSpec, temperature=0.1 # Low temperature for high accuracy and structure compliance ) )

return response.text `

Step 3: Handle the Output Formatting and CLI Args

Now, let's assemble the raw structured JSON into a classic OpenAPI 3.0.0 YAML or JSON specification format that your external tools actually expect. We will parse Gemini's custom structured response and map it directly to standard Swagger-compliant formats.

Add this helper function and standard runner boilerplate to your script:

`python def format_to_standard_openapi(custom_spec_json: str) -> dict: data = json.loads(custom_spec_json) # Reconstruct the spec to match native OpenAPI 3.0.0 spec structure openapi_spec = { "openapi": data.get("openapi", "3.0.0"), "info": { "title": data.get("info_title", "API Documentation"), "version": data.get("info_version", "1.0.0"), "description": data.get("info_description", "") }, "paths": {} }

for endpoint in data.get("endpoints", []): path = endpoint.get("path") method = endpoint.get("method", "get").lower() if path not in openapi_spec["paths"]: openapi_spec["paths"][path] = {} path_item = { "summary": endpoint.get("summary", ""), "description": endpoint.get("description", ""), "parameters": [], "responses": {} }

Process parameters for param in endpoint.get("parameters", []): path_item["parameters"].append({ "name": param.get("name"), "in": param.get("in_"), "required": param.get("required", False), "schema": {"type": param.get("type", "string")}, "description": param.get("description", "") })

Process Request Body if present if endpoint.get("request_body_example"): try: body_json = json.loads(endpoint["request_body_example"]) path_item["requestBody"] = { "content": { "application/json": { "schema": {"type": "object"}, # Simple fallback schema "example": body_json } } } except json.JSONDecodeError: pass

Process responses for resp in endpoint.get("responses", []): status = resp.get("status_code", "200") resp_data = { "description": resp.get("description", "Success") } if resp.get("schema_example"): try: resp_data["content"] = { "application/json": { "example": json.loads(resp["schema_example"]) } } except json.JSONDecodeError: pass path_item["responses"][status] = resp_data

openapi_spec["paths"][path][method] = path_item

return openapi_spec

if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python converter.py <path_to_markdown_file> [output_json_path]") sys.exit(1)

input_path = sys.argv[1] output_path = sys.argv[2] if len(sys.argv) > 2 else "openapi_spec.json"

if not os.path.exists(input_path): print(f"Error: File '{input_path}' not found.") sys.exit(1)

with open(input_path, "r", encoding="utf-8") as f: markdown_data = f.read()

print("Sending Markdown documentation to Gemini 1.5 Flash...") try: structured_json = convert_markdown_to_spec(markdown_data) final_spec = format_to_standard_openapi(structured_json) with open(output_path, "w", encoding="utf-8") as out_f: json.dump(final_spec, out_f, indent=2) print(f"\nSuccess! Valid OpenAPI spec generated and saved to: {output_path}") except Exception as e: print(f"\nAn error occurred during conversion: {e}", file=sys.stderr) print("For API issues, check out the troubleshooting guides on Tickd:", file=sys.stderr) print("https://tickd.ai/platforms/gemini/articles", file=sys.stderr) sys.exit(1) `

To make sure you understand the core mechanics of structured parsing APIs, take a moment to read our comprehensive guide on LLM data mapping strategies in our AI glossary.

Step 4: Run a Test Migration

Let’s put our parser to the test. Create a sample markdown file named api_docs.md that represents a typical, slightly messy documentation draft:

`markdown # User Management Service API (v1.2.0)

This is a simple system for managing internal platform users.

GET /users/{id} Retrieves details of a specific user.

Parameters: - id (path parameter, integer, required): The unique identifier of the user account.

Responses: - 200: Success. Returns user object. `json { "id": 109, "username": "johndoe", "role": "administrator" } ` - 404: User not found.

POST /users Creates a new profile.

Request Body Example: `json { "username": "janedoe", "role": "member" } `

Response: - 201: User created successfully. `

Now, run your script from the terminal:

`bash python converter.py api_docs.md openapi.json `

Open up openapi.json. You will see a perfectly structured, flat OpenAPI 3.0.0 compliant spec, complete with JSON objects constructed from your markdown code blocks, ready to be dropped into Postman or your documentation hub of choice.

Troubleshooting and Model Limitations

Gemini 1.5 Flash is exceptionally fast, but it can occasionally trip on nested schemas or highly ambiguous route parameters (like complex query arrays). Ensure your markdown documentation lists parameters using explicit terms like query, path, or header so the model doesn't have to guess context.

If you run into issues with your API key, local environment, or parsing timeout rates, you can find a suite of platform-specific workarounds and debug checklists at our dedicated hub: /platforms/gemini/articles.

By leveraging structured outputs, you can bypass regex-heavy custom parsers entirely. Hook this small Python script into your Git pre-commit workflow, and you'll never have to manually edit a bloated OpenAPI YAML configuration ever again.

pythongeminiopenapiapi-designautomations

Keep going

Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.

Set up your AI once so you repeat yourself less.

Try AI Setup Profile

Tickd is independent and not affiliated with any AI company.