Skip to main content
Lesson 3 of 16 · Fine-Tuning OpenAI Models

Data format: JSONL conversations

Lesson 3 of 16 Updated November 2, 2026 Advanced
On this page
  1. The JSONL format
  2. Conversation structure
  3. Multi-turn conversations
  4. Weighting with weight
  5. JSONL file creation script
  6. Key takeaways

Update November 2, 2026 — Fine-tuning availability. OpenAI’s self-service fine-tuning is being phased out: since May 7, 2026, organizations that have never used it can no longer create jobs; since July 2, you must have used a fine-tuned model within the last 60 days; and starting January 6, 2027, no customer will be able to launch new jobs. Already trained models remain usable until their base model is retired. This course remains useful for understanding and leveraging an existing model; for new projects, prioritize prompt engineering, RAG, and evaluations.

The JSONL format

JSONL (JSON Lines) is the required format for the OpenAI fine-tuning API. Each line in the file is an independent JSON object representing a complete training example. This structure lets you stream a corpus of tens of thousands of examples without ever loading the entire file into memory.

{"messages": [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}
{"messages": [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}

Fundamental rule: one line = one JSON object = one example. Do not include line breaks inside the JSON object itself. In other words, “pretty-printed” indented JSON from your editor will break the file—this is the most common cause of upload rejection.

Conversation structure

Each example is an object with a messages key containing an array of messages. Every message has a role and a content. Only three roles are accepted:

  • system: global instructions for the assistant (optional, but recommended)
  • user: the user’s message
  • assistant: the response the model must learn to produce

The minimal format consists of a single user/assistant exchange. This is sufficient when the desired behavior doesn’t depend on specific framing, such as for a model dedicated to a single task.

{
  "messages": [
    {"role": "user", "content": "Hello, how are you?"},
    {"role": "assistant", "content": "Hello! I'm ready to help. What can I do for you?"}
  ]
}

As soon as the model needs to adopt a specific role, add a system message—and keep it identical across all your examples. Note in the example below that the instruction “always offer a demo appointment” is actually honored in the assistant’s response: the system message describes a constraint, and the example shows what it looks like when applied.

{
  "messages": [
    {
      "role": "system",
      "content": "You are a sales assistant for TechCorp. Respond concisely and professionally. Always offer a demo appointment."
    },
    {
      "role": "user",
      "content": "I'm looking for a CRM solution for my team of 50 people."
    },
    {
      "role": "assistant",
      "content": "Our TechCorp CRM is designed for teams of 20 to 200 people, with contact management, sales pipeline, and integrated reporting. For a team of 50, I recommend the Business plan at €29/user/month. Would you like to schedule a personalized demo this week?"
    }
  ]
}

Multi-turn conversations

You can include conversations with multiple exchanges. The model then learns to maintain consistency across turns: here, the second response builds on step 2 announced in the first, something an isolated example couldn’t teach.

{
  "messages": [
    {
      "role": "system",
      "content": "You are a technical assistant for a payment API."
    },
    {
      "role": "user",
      "content": "How do I integrate your payment API?"
    },
    {
      "role": "assistant",
      "content": "Integration happens in 3 steps: 1) Install the SDK with pip install paycorp-sdk, 2) Configure your API key, 3) Create your first payment. Which step would you like to start with?"
    },
    {
      "role": "user",
      "content": "How do I configure the API key?"
    },
    {
      "role": "assistant",
      "content": "Get your key from the dashboard (Settings > API Keys), then configure it in your code:\n\nfrom paycorp import PayCorp\nclient = PayCorp(api_key=\"your_key\")\n\nUse the test key (prefix sk_test_) in development and the live key (sk_live_) in production. Never commit your keys to source code."
    }
  ]
}

Weighting with weight

You can tell the model which assistant messages are most important by adding a weight field:

{
  "messages": [
    {"role": "user", "content": "Summarize this document."},
    {"role": "assistant", "content": "Here is the summary...", "weight": 1}
  ]
}

A weight of 0 means the model won’t train on that specific assistant message; the default value is 1. The typical use case is multi-turn conversations where only the final turn matters: you keep previous exchanges because they provide necessary context, but you don’t want the model to learn to reproduce them—just set them to 0.

JSONL file creation script

In practice, you’ll never write this file by hand. The function below serializes a list of examples and protects you from the two most common pitfalls: forgetting the final line break and mishandling accent escaping.

import json

def create_jsonl_file(examples: list[dict], path: str):
    """Creates a JSONL file from a list of examples."""
    with open(path, "w", encoding="utf-8") as f:
        for example in examples:
            line = json.dumps(example, ensure_ascii=False)
            f.write(line + "\n")
    print(f"File created: {path} ({len(examples)} examples)")

# Your examples
examples = [
    {
        "messages": [
            {"role": "system", "content": "TechCorp customer support assistant."},
            {"role": "user", "content": "My account is locked."},
            {"role": "assistant", "content": "I understand your frustration. To unlock your account, I need your registration email address. I'll check the status and guide you through the reactivation process."}
        ]
    },
    {
        "messages": [
            {"role": "system", "content": "TechCorp customer support assistant."},
            {"role": "user", "content": "How do I change my password?"},
            {"role": "assistant", "content": "To change your password: go to Settings > Security > Change Password. Enter your current password, then the new one (minimum 12 characters, with uppercase, number, and special character). Confirm and click Save."}
        ]
    }
]

create_jsonl_file(examples, "training_data.jsonl")

The errors you’ll encounter are always the same, and all can be detected before upload. A missing line break between two objects merges two examples into one unreadable line. Malformed JSON—missing quote, extra comma—fails the entire line. An empty content triggers a validation error on OpenAI’s side, as does an invented role: only system, user, and assistant are accepted. Finally, always write in UTF-8 with ensure_ascii=False, otherwise your accented characters will turn into escape sequences that the model will conscientiously learn to reproduce.

Key takeaways

  • One JSONL line = one JSON object = one complete conversation
  • Each conversation contains at least one user message and one assistant message
  • The system message is optional but recommended for consistency
  • Use weight: 0 to exclude certain turns from training in multi-turn conversations
  • Always validate your JSONL file before uploading