I’ve spent the last year building content workflows that turn raw ideas into structured, publishable assets. The hardest part isn’t the writing—it’s the repeatability. When you’re producing dozens of pieces per month, you need a system that converts research notes into consistent, schema-valid JSON that downstream pipelines can consume. This post walks through how I used DeepSeek’s API alongside a rigid JSON schema to automate my content generation pipeline, cutting production time nearly in half.

[Loading prompt card for DeepSeek Chat...]

The Problem I Was Trying to Solve§

My team manages a technical blog that publishes four in-depth articles per week. Each article needs metadata: title, slug, tags, excerpt, reading time, and a structured table of contents. We also maintain a content database that expects every piece to conform to a specific JSON schema. Manually filling that metadata is tedious and error-prone. I’d watch writers rewrite titles five times, and editors manually tag posts with inconsistent terminology.

The real bottleneck wasn’t the prose—it was the structure. A human can write a compelling 1,200-word piece, but when that piece has to fit into an automated distribution pipeline, you need consistent metadata. I wanted a system where I could dump a rough draft or even just a topic, and get back a fully structured article with all the necessary fields populated—ready to drop into our CMS.

I tried using Claude and GPT-4 for this before. They often produced good prose, but the JSON output was frequently malformed—missing commas, unescaped quotes, or extra fields that didn’t match the schema. I’d have to run a validation step and repair the JSON manually, which defeated the purpose. DeepSeek’s ability to follow structured instructions with high reliability made me wonder if it could handle the whole job.

[Loading prompt card for Claude...]

Tools and Setup§

For this project, I used the following stack:

  • DeepSeek API (deepseek-chat model) via Python requests
  • Pydantic for schema definition and validation
  • Jinja2 for prompt templating
  • **Cursor** as my IDE for rapid iteration
  • **Perplexity** for research aggregation when I needed source material for the articles
  • Postman for quick API tests before writing production code
[Loading prompt card for Perplexity AI...]

The core idea is simple: define the expected output structure as a Pydantic model, then ask DeepSeek to return JSON that matches that model. I used a trick that many people overlook—I embedded the schema definition directly in the system prompt, not just the user prompt. This gives the model a clear target from the start.

Here’s the setup I used (simplified for illustration):

from pydantic import BaseModel, Field
from typing import List, Literal

class Article(BaseModel):
    title: str = Field(description="SEO-optimized title under 60 chars")
    slug: str = Field(pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
    tags: List[str] = Field(min_items=2, max_items=5)
    excerpt: str = Field(description="One-sentence summary under 160 chars")
    reading_time: int = Field(ge=3, le=20)
    status: Literal["draft", "review", "published"] = "draft"
    toc: List[dict] = Field(description="List of heading objects with id and text")
    body: str = Field(description="Markdown content")

I then created a prompt template that includes this schema. The trick is to show the model an example output, not just the schema. Few-shot examples dramatically improve JSON compliance.

Step-by-Step: What I Actually Did§

Step 1: Define the output contract. I wrote a Pydantic model that exactly matched the fields I needed. The key was to give each field a clear description and validation constraints. This isn’t just for the API—it also gives the model hints about what you expect.

Step 2: Build a prompt template. I created a Jinja2 template that takes a topic or a source text and inserts it into a detailed prompt. The prompt explicitly says: "Return ONLY valid JSON, no markdown, no explanation." I also included two examples of correct outputs—one for a short article and one for a longer one.

Step 3: Add procedural instructions. Beyond the schema, I gave DeepSeek rules like: "Use active voice", "Avoid clichés", "Tag with at least three topics from the provided list", and "Write the excerpt as if it could appear in an RSS feed." These forced the model to think about the audience, not just the structure.

Step 4: Validate and retry. I wrapped the API call in a loop that checks if the returned JSON passes Pydantic validation. If it fails, I send the error message back to DeepSeek with a request to fix it. This self-healing loop increased success rate from around 60% on the first attempt to over 98% after two retries.

Step 5: Automate the pipeline. I scheduled a script that pulls research notes from a queue (in this case, a simple Airtable table), sends each to DeepSeek, validates the output, and then pushes the structured article into a GitHub repo as a markdown file with frontmatter.

Code Samples / Prompts Used§

Here’s the exact function I used for generating an article from a topic:

import json
import openai

client = openai.OpenAI(
    api_key="your_deepseek_api_key",
    base_url="https://api.deepseek.com/v1"
)

def generate_article(topic: str, source_material: str = "") -> dict:
    system_prompt = """You are a technical content strategist. You always output valid JSON matching the provided schema. Do not wrap JSON in markdown or code blocks.

Schema:
{
  "title": "string, under 60 chars",
  "slug": "lowercase-with-hyphens",
  "tags": ["string"],
  "excerpt": "string, under 160 chars",
  "reading_time": "integer",
  "toc": [{"id": "section-id", "text": "Section Title"}],
  "body": "markdown string"
}

Rules:
- Use active voice and concrete examples.
- For the TOC, include at least 3 sections.
- The body must contain at least 800 words.
- Return ONLY JSON.
"""

    user_prompt = f"""Topic: {topic}\n\nSource material:\n{source_material or 'None—use your own knowledge'}\n\nGenerate a complete article now."""

    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}
        ],
        response_format={"type": "json_object"},
        temperature=0.7,
        max_tokens=2000
    )

    # Parse and validate
    try:
        article = json.loads(response.choices[0].message.content)
        # Add your own validation logic here (Pydantic)
        return article
    except json.JSONDecodeError as e:
        # If JSON fails, raise an error that triggers a retry
        raise ValueError(f"Invalid JSON: {e}")

I also wrote a retry wrapper that feeds the validation error back to the model:

def generate_with_retry(topic: str, max_retries: int = 3):
    for attempt in range(max_retries):
        try:
            return generate_article(topic)
        except Exception as e:
            print(f"Attempt {attempt+1} failed: {e}")
            # Send the error message back to DeepSeek with a request to fix
            # Implementation omitted for brevity but simple—just append the error to the prompt
    raise RuntimeError("Failed to generate valid article")

One prompt that worked especially well was this few-shot example inside the system prompt:

{
  "title": "How to Build a REST API with FastAPI",
  "slug": "how-to-build-rest-api-with-fastapi",
  "tags": ["python", "api", "fastapi"],
  "excerpt": "Learn how to create a production-ready REST API using FastAPI in under 30 minutes.",
  "reading_time": 8,
  "toc": [
    {"id": "introduction", "text": "Introduction"},
    {"id": "setup", "text": "Setup"},
    {"id": "defining-models", "text": "Defining Models"},
    {"id": "creating-routes", "text": "Creating Routes"},
    {"id": "testing", "text": "Testing"}
  ],
  "body": "# Introduction\n..."
}

Including that example in the system prompt made the model more likely to follow the exact field types and formatting.

What Worked Well§

DeepSeek’s JSON adherence was surprisingly strong. Once I switched to response_format: json_object, the output was almost always valid JSON. The first-attempt success rate hovered around 85% for well-specified schemas. After adding the retry loop with error feedback, it went to 99%.

The schema descriptions make the model “think” about each field. When I wrote detailed descriptions in the Pydantic model, the model produced richer, more relevant content. For example, specifying that excerpt should be “one sentence that entices the reader” led to better excerpts than a generic “summary” description.

Automating the TOC creation was a time-saver. I used to manually create anchors for each heading. Now DeepSeek generates a toc list that matches the actual headings in the body, and I can programmatically verify that each id exists in the markdown.

The retry loop is the unsung hero. When I started, I thought I’d need to validate and fix the JSON myself. But simply sending the error message back to the model with instructions like “Fix the missing comma at position 123” turned out to be faster than writing custom repair scripts.

What Failed and Why§

**First attempt without response_format was messy.** Initially, I forgot to set response_format: json_object and DeepSeek sometimes returned conversational text like “Sure, here’s your article: { ... }”. That extra text broke my parser. Adding the parameter and explicitly instructing “do not include any explanations” fixed it.

Overly complex schemas caused hallucination. When I first tried to include a related_links array with objects containing url, title, and source, the model started inventing URLs. I had to remove that field and generate it separately using a search API. Lesson: if a field requires external data not present in the prompt, don’t ask the model to fill it.

Long articles made the model lose track. When I asked for a 2,000-word piece, DeepSeek would often repeat sections or stray from the outline. The body would be structurally inconsistent with the TOC. I solved this by generating the TOC first, then generating each section in a separate call, and finally stitching them together. This increased the number of API calls but improved quality dramatically.

Temperature matters more than you think. I initially used temperature=0.9 to encourage creativity, but the output became less reliable in following the schema. Dropping to 0.7 was a sweet spot for my use case—it kept the writing natural while avoiding too much randomness in structure.

Results and Takeaways§

After implementing this pipeline, I reduced the average time to produce a structured article from 4 hours to about 1.5 hours. The time savings came mostly from eliminating manual metadata entry and from the fact that DeepSeek’s initial drafts were good enough to edit rather than rewrite.

Accuracy also improved. We had a 12% error rate in metadata before; now it’s under 1% after validation. Our content database no longer has duplicate or mistagged entries.

One unexpected win: the structured TOC has made our internal review process easier. Editors can see the outline at a glance and approve or reject the structure before reading the full draft.

Key Takeaways:

  • Embed the schema in the system prompt with examples—this is the most effective way to get consistent structured outputs from DeepSeek.
  • **Combine response_format: json_object with a Pydantic validation retry loop** to achieve near-perfect output reliability.
  • Separate generation of outline and body to maintain coherence in long-form content.
  • Don’t ask the model to produce external data (like URLs) unless you’re prepared to verify it; keep the schema limited to what the model can know from the prompt.

Try It Yourself§

If you want to build a similar pipeline, start small. Pick one content type—like a blog post—and define a minimal schema. Use the Pydantic model I showed above or create your own. Then run a few tests with topic variations to see how DeepSeek responds.

A few practical recommendations:

  1. Use a dedicated API key for this workflow so you can monitor usage separately.
  2. Log all API responses to a local file for debugging. You’ll quickly spot patterns in failures.
  3. Start with a retry limit of two—beyond that, the model usually can’t fix the issue, and it’s better to fail manually.
  4. Write a custom validator that checks not just JSON structure but also semantic rules (e.g., word count, tag length). This catches problems before they enter your CMS.

I’ve open-sourced a simplified version of the pipeline in a GitHub repo. Feel free to adapt it. If you hit a wall, the most common issues are prompt clarity and schema complexity. Simplify both.

Programmatic content generation isn’t about replacing writers—it’s about giving them tools that remove overhead. With DeepSeek and structured schemas, you can turn a topic into a production-ready asset in minutes, not days. That’s the real payoff.