Skip to main content
Lesson 2 of 16 · Production: Batch, Optimization, and Observability

JSONL Format, custom_id, and File Upload

Lesson 2 of 16 Updated November 3, 2026 Advanced
On this page
  1. The JSONL Format: The Backbone of Batch Inference
  2. JSONL Request Structure
  3. Generating a JSONL File in Python
  4. Uploading the File to Mistral
  5. File Validation
  6. Limits and Constraints
  7. Key Takeaways

Updated November 3, 2026: With SDK mistralai 3.7.0, files.upload expects file={"file_name": …, "content": …} (passing a tuple raises a ValidationError before any data is sent), and file size is available in size_bytes.

The JSONL Format: The Backbone of Batch Inference

JSONL (JSON Lines) is the standard format Mistral uses to structure batch requests. Each line in the file is a standalone JSON object representing a single request. This design is intentional: you can stream-generate a million-line file without loading it all into memory, read it back line by line, and inspect specific entries manually when a request fails. Unlike a single massive JSON array, which forces you to load the entire file just to verify one entry, JSONL lets you work at the line level.

JSONL Request Structure

Each line in your JSONL file must contain two required fields:

{
  "custom_id": "req-001",
  "body": {
    "max_tokens": 100,
    "messages": [
      {"role": "user", "content": "Summarize this document in 3 points."}
    ]
  }
}

The custom_id is a unique identifier you define for each request. It is what makes the results usable. Mistral does not guarantee processing order: the output file might start with request 8,472. Without a custom_id, you end up with a pile of responses and no way to know which document each one corresponds to. Make it a habit to derive this ID from your own reference system—doc-001, email-2024-03-15-042, product-sku-ABC123—rather than using an anonymous counter. When a response causes issues, you can trace it back to the source without running a SQL query. Two practical rules apply here: IDs must be unique within the file, or processing will return an error, and they should be reasonably short, since a 200-character identifier makes logs unreadable.

The body field requires no new learning: it contains the exact structure you would use for a synchronous API call to the target endpoint. If you are targeting /v1/chat/completions, the body is identical to what you would pass in a client.chat.complete() call. A prompt validated interactively translates directly into a batch job.

Generating a JSONL File in Python

Here is how to programmatically generate a JSONL file from a list of documents:

import json

documents = [
    {"id": "doc-001", "text": "Artificial intelligence is transforming..."},
    {"id": "doc-002", "text": "Deep neural networks..."},
    {"id": "doc-003", "text": "Natural language processing..."},
]

with open("batch_requests.jsonl", "w", encoding="utf-8") as f:
    for doc in documents:
        request = {
            "custom_id": doc["id"],
            "body": {
                "model": "mistral-small-latest",
                "max_tokens": 200,
                "messages": [
                    {
                        "role": "system",
                        "content": "You are an assistant that summarizes texts."
                    },
                    {
                        "role": "user",
                        "content": f"Summarize this text: {doc['text']}"
                    }
                ]
            }
        }
        f.write(json.dumps(request, ensure_ascii=False) + "\n")

Note the ensure_ascii=False parameter. Without it, json.dumps escapes accented characters as \uXXXX sequences. The file remains valid, but becomes impossible for humans to read, turning any debugging on a French corpus into a decryption exercise. Make this your default from the first line of code.

Uploading the File to Mistral

In production, upload via the Python API:

from mistralai.client import Mistral

client = Mistral(api_key="your-api-key")

# Upload the JSONL file
with open("batch_requests.jsonl", "rb") as f:
    batch_file = client.files.upload(
        file={"file_name": "batch_requests.jsonl", "content": f},
        purpose="batch"
    )

print(f"Uploaded file: {batch_file.id}")
print(f"Size: {batch_file.size_bytes} bytes")
print(f"Purpose: {batch_file.purpose}")

The purpose="batch" parameter tells Mistral this file is intended for batch processing, not for other features like fine-tuning or RAG. This declaration of intent dictates what happens next: a file uploaded with the wrong purpose will be rejected by the job.

For a quick test or for non-technical teams, AI Studio offers the same service via the GUI:

  1. Go to console.mistral.ai/build/files
  2. Click “Upload”
  3. Select your JSONL file
  4. Choose the purpose “Batch Processing”

The interface returns a file_id, exactly like the API, and this is the identifier you use to create the batch job.

File Validation

A million-line batch that fails at line 300 due to a duplicate custom_id costs you a full wait cycle. A few seconds of local validation prevents this loss. The following script performs a single pass to check JSON validity, the presence of the two required fields, and ID uniqueness:

import json

errors = []
seen_ids = set()

with open("batch_requests.jsonl", "r", encoding="utf-8") as f:
    for line_num, line in enumerate(f, 1):
        try:
            data = json.loads(line)
        except json.JSONDecodeError as e:
            errors.append(f"Line {line_num}: Invalid JSON - {e}")
            continue

        # Check required fields
        if "custom_id" not in data:
            errors.append(f"Line {line_num}: missing custom_id")
        if "body" not in data:
            errors.append(f"Line {line_num}: missing body")

        # Check custom_id uniqueness
        cid = data.get("custom_id")
        if cid in seen_ids:
            errors.append(f"Line {line_num}: duplicate custom_id '{cid}'")
        seen_ids.add(cid)

if errors:
    print(f"{len(errors)} error(s) found:")
    for e in errors:
        print(f"  - {e}")
else:
    print(f"File valid: {len(seen_ids)} requests ready.")

Make this a systematic step in your preparation pipeline, executed just before upload.

Limits and Constraints

File batching caps at one million lines per file, meaning you must split larger corpora into multiple submissions. Encoding must be UTF-8, and the format strictly requires one JSON object per line: a JSON object spread across multiple lines, as produced by a misplaced json.dumps(..., indent=2), breaks server-side parsing. Finally, remember the two rules already mentioned, as violating them causes the job to fail: the custom_id must be unique within the file, and the body must match the schema of the target endpoint.

Key Takeaways

  • JSONL structures your batch requests with one JSON object per line
  • Each request requires a unique custom_id and a body matching the target endpoint
  • Upload via the Python API (client.files.upload) or through AI Studio
  • Always validate your file before submission to avoid processing errors
  • Use ensure_ascii=False for French and accented characters