csvjson
Salesforce API

Convert Salesforce JSON to CSV

Every Salesforce SOQL query result wraps records in an envelope and attaches an attributes object to each row. Stripping those and flattening relationship fields is all it takes to get clean CSV.

Salesforce REST API query results follow a consistent pattern: a totalSize and done flag at the top level, a records array where each record has an attributes object (containing type and url), and any relationship fields as nested objects. This structure is the same whether you're querying Contacts, Leads, Opportunities, or custom objects. Once you know how to handle it, any Salesforce export follows the same logic.

Want to skip the code? Paste your JSON directly into the converter — it handles nested objects, arrays, and large files automatically.

Open JSON to CSV Converter

What the API returns

Salesforce REST API SOQL query result — contacts with related Account

{
  "totalSize": 2,
  "done": true,
  "records": [
    {
      "attributes": {
        "type": "Contact",
        "url": "/services/data/v59.0/sobjects/Contact/003Dn000004ABCDE"
      },
      "Id": "003Dn000004ABCDE",
      "FirstName": "Alice",
      "LastName": "Chen",
      "Email": "alice@example.com",
      "Title": "VP Engineering",
      "Department": "Engineering",
      "Phone": "512-555-0100",
      "Account": {
        "attributes": {
          "type": "Account",
          "url": "/services/data/v59.0/sobjects/Account/001Dn000003XYZAB"
        },
        "Name": "Acme Corp",
        "Industry": "Technology"
      },
      "CreatedDate": "2025-01-15T09:30:00.000+0000",
      "LastModifiedDate": "2025-03-20T14:22:00.000+0000"
    }
  ]
}

Field mapping: JSON path → CSV column

JSON pathCSV columnNotes
Idsalesforce_id18-char Salesforce record ID
FirstNamefirst_name
LastNamelast_name
Emailemail
Titletitle
Departmentdepartment
Phonephone
Account.Nameaccount_nameRelationship field — nested object
Account.Industryaccount_industry
CreatedDatecreated_dateISO 8601 with timezone
LastModifiedDatelast_modified
attributes(drop)Always remove from output

Drop the 'attributes' key from every record — it contains internal Salesforce metadata (type name and API URL) that doesn't belong in a CSV export.

Python conversion

Convert Salesforce SOQL query result to CSV

import json
import pandas as pd

with open("sf_contacts.json") as f:
    data = json.load(f)

records = data["records"]

def flatten_sf_record(record):
    """Flatten a Salesforce record, dropping attributes and promoting relationship fields."""
    out = {}
    for key, value in record.items():
        if key == "attributes":
            continue  # drop internal metadata
        if isinstance(value, dict) and "attributes" in value:
            # Relationship field — flatten it with the parent key as prefix
            for sub_key, sub_val in value.items():
                if sub_key == "attributes":
                    continue
                out[f"{key}_{sub_key}"] = sub_val
        else:
            out[key] = value
    return out

flat_records = [flatten_sf_record(r) for r in records]

df = pd.DataFrame(flat_records)

# Clean up date columns — Salesforce uses ISO 8601 with timezone offset
for col in df.select_dtypes(include="object").columns:
    if df[col].str.match(r"\d{4}-\d{2}-\d{2}T", na=False).any():
        df[col] = pd.to_datetime(df[col], errors="coerce", utc=True).dt.strftime("%Y-%m-%d %H:%M")

df.to_csv("contacts.csv", index=False, encoding="utf-8-sig")
print(f"Exported {len(df)} contacts")

Query and export using simple_salesforce (handles auth and pagination)

from simple_salesforce import Salesforce
import pandas as pd

sf = Salesforce(
    username="user@example.com",
    password="your_password",
    security_token="your_token",
)

# SOQL query — select the fields you need explicitly
result = sf.query_all("""
    SELECT Id, FirstName, LastName, Email, Title, Department,
           Phone, Account.Name, Account.Industry,
           CreatedDate, LastModifiedDate
    FROM Contact
    WHERE IsDeleted = false
    ORDER BY LastName
""")

print(f"Total records: {result['totalSize']}")

def flatten_sf_record(record):
    out = {}
    for key, value in record.items():
        if key == "attributes":
            continue
        if isinstance(value, dict) and "attributes" in value:
            for sub_key, sub_val in value.items():
                if sub_key != "attributes":
                    out[f"{key}_{sub_key}"] = sub_val
        else:
            out[key] = value
    return out

rows = [flatten_sf_record(r) for r in result["records"]]
df = pd.DataFrame(rows)
df.to_csv("sf_contacts.csv", index=False, encoding="utf-8-sig")
print(f"Exported {len(df)} rows")

Export Opportunities with multi-level relationships

from simple_salesforce import Salesforce
import pandas as pd

sf = Salesforce(username="...", password="...", security_token="...")

result = sf.query_all("""
    SELECT Id, Name, StageName, Amount, CloseDate,
           Probability, Type, LeadSource,
           Account.Name, Account.Industry, Account.BillingCity,
           Owner.Name, Owner.Email
    FROM Opportunity
    WHERE IsClosed = false
""")

def flatten_sf_record(record):
    out = {}
    for key, value in record.items():
        if key == "attributes":
            continue
        if isinstance(value, dict) and "attributes" in value:
            for sub_key, sub_val in value.items():
                if sub_key != "attributes":
                    out[f"{key}_{sub_key}"] = sub_val
        else:
            out[key] = value
    return out

df = pd.DataFrame([flatten_sf_record(r) for r in result["records"]])

# Amount is already a float in Salesforce (unlike Stripe's cents)
df["Amount"] = pd.to_numeric(df["Amount"], errors="coerce")
df["CloseDate"] = pd.to_datetime(df["CloseDate"], errors="coerce").dt.strftime("%Y-%m-%d")

df.to_csv("open_opportunities.csv", index=False, encoding="utf-8-sig")
print(f"Exported {len(df)} opportunities")

Common issues with this API

The 'attributes' object on every record must be dropped

Salesforce attaches an 'attributes' dict to every record containing the object type and REST API URL. It's internal metadata — if you include it in json_normalize, you'll get 'attributes.type' and 'attributes.url' columns in your CSV that don't belong there. Always filter it out before flattening.

# With json_normalize — drop by column after
df = pd.json_normalize(records, sep="_")
drop = [c for c in df.columns if c.startswith("attributes") or c.endswith("_attributes")]
df = df.drop(columns=drop)

Relationship fields come back as nested objects, not null

If a Contact has no Account, the Account field is null. If it has an Account, the field is a nested object. When using json_normalize, null relationships produce NaN; present relationships produce flattened columns. Both are handled correctly by the flatten_sf_record function above.

SOQL SELECT * is not supported

Unlike SQL, SOQL doesn't support SELECT *. You must specify every field you want. Use the Salesforce Object Reference or the describe API endpoint to get a list of all available fields for an object: sf.Contact.describe()['fields'].

# Get all field names for an object
fields = [f["name"] for f in sf.Contact.describe()["fields"]]
print(", ".join(fields[:20]))  # preview first 20

Frequently asked questions

What's the maximum number of records SOQL can return?

A single SOQL query returns up to 2,000 records. For larger datasets, use query_all() from simple_salesforce (which follows the nextRecordsUrl automatically) or set queryMore=True. The API itself has no hard record limit — pagination handles the rest.

How do I export a Salesforce report to CSV instead of querying the API?

Salesforce reports can be exported directly: open the report, click the export button, and choose 'Details only' with CSV format. This avoids the API entirely and is the fastest path for ad-hoc exports. Use the API approach for automated or scheduled exports.

Why do Salesforce IDs have 15 and 18 character variants?

Salesforce uses 15-character case-sensitive IDs in the UI and 18-character case-insensitive IDs in the API. The 18-char version has a 3-char checksum suffix. Always use 18-char IDs in exports and joins — they're safe to use case-insensitively.

Does the online converter work with Salesforce JSON?

Yes. Save the SOQL query result JSON to a file and paste it into the JSON to CSV converter. The converter flattens the records array and nested relationship objects automatically. You'll want to manually remove the attributes columns from the output.

Related tools