# How transform.py Normalizes Loose LLM JSON to JSON Resume Format

> Learn how transform.py in hiring-agent normalizes loose LLM JSON to JSON Resume format. Discover its pipeline for dates, skills, projects, and personal details.

- Repository: [HackerRank/hiring-agent](https://github.com/interviewstreet/hiring-agent)
- Tags: how-to-guide
- Published: 2026-07-22

---

**The [`transform.py`](https://github.com/interviewstreet/hiring-agent/blob/main/transform.py) module in the interviewstreet/hiring-agent repository converts unstructured LLM-generated JSON into strict JSON-Resume compliance through a pipeline of specialized transformer functions that handle dates, skills, projects, and personal details.**

The interviewstreet/hiring-agent project relies on [`transform.py`](https://github.com/interviewstreet/hiring-agent/blob/main/transform.py) to bridge the gap between unpredictable LLM outputs and structured resume data. This module normalizes loose JSON into the JSON-Resume specification, ensuring downstream components can safely consume parsed candidate information according to the strict schema defined in [`models.py`](https://github.com/interviewstreet/hiring-agent/blob/main/models.py).

## Entry Point and Orchestration Logic

The normalization process begins with **`transform_parsed_data`**, the primary entry point implemented at lines 6-87 in [`transform.py`](https://github.com/interviewstreet/hiring-agent/blob/main/transform.py).

This function first detects whether the input is a dictionary. When the payload contains a top-level `basics` section alongside other resume sections, it builds a complete JSON-Resume document by invoking dedicated transformers for each field (lines 6-39). If the input contains only a single top-level section—such as only `work` or only `skills`—it falls back to a minimal document structure containing just that section (lines 40-87). The function returns a normalized dictionary that strictly matches the JSON-Resume schema.

## Normalizing Personal Information

### The Basics Section (`transform_basics`)

Located at lines 25-52, **`transform_basics`** handles personal profile normalization. The function cleans the `profiles` list and infers the `network` name from a URL when missing. It extracts usernames using the **`extract_username_from_url`** helper, ensuring that social media links conform to the JSON-Resume `basics` specification with proper network classification and username fields.

## Professional Experience and Education

### Work Experience (`transform_work_experience`)

The **`transform_work_experience`** function (lines 75-121) guarantees that every work entry contains the required keys: `name`, `position`, `url`, `startDate`, `endDate`, `summary`, and `highlights`. It handles description fields that may arrive as lists, concatenating them into summary strings. The function parses human-readable date ranges such as "Jan-Mar 2021" using **`parse_date_range`**, converting them into ISO-compliant date strings.

### Education Entries (`transform_education`)

Implemented at lines 42-74, **`transform_education`** converts education entries into the required fields: `institution`, `url`, `area`, `studyType`, `startDate`, `endDate`, `score`, and `courses`. The function detects degree strings that include both study type and area (e.g., "B.Sc., Computer Science") and splits them appropriately. It also processes year ranges via `parse_date_range` to ensure consistent date formatting.

### Volunteer Work (`transform_organizations`)

At lines 24-40, **`transform_organizations`** maps generic organization data into the JSON-Resume `volunteer` format. This includes normalizing fields for organization name, position, URL, start dates, and end dates to match the specification's volunteer section requirements.

## Skills, Projects, and Achievements

### Skills Normalization (`transform_skills_comprehensive`)

The **`transform_skills_comprehensive`** function (lines 47-75) accepts several possible key groups from LLM outputs: `skills`, `librariesFrameworks`, `toolsPlatforms`, and `databases`. When `skills` arrives as a flat list of strings, the function wraps them under a default "Programming Languages" category. Otherwise, it delegates to **`transform_skills`**, which builds proper category objects containing `name`, `level`, and `keywords` arrays.

### Projects Processing (`transform_projects_comprehensive`)

Located at lines 78-90, **`transform_projects_comprehensive`** handles both the generic `projects` field and the `projectsOpenSource` field. The function splits composite project titles—such as `"MyApp | Python, Flask"`—into clean names and extracted technology lists. It guarantees output fields including `name`, `startDate`, `endDate`, `description`, `highlights`, `url`, `technologies`, and `skills`.

### Awards and Achievements (`transform_achievements`)

At lines 77-92, **`transform_achievements`** normalizes award objects to contain `title`, `date`, `awarder`, and `summary` fields, ensuring compliance with the JSON-Resume awards specification regardless of the input structure provided by the LLM.

## Date Parsing Utilities

The **`parse_date_range`** utility (lines 112-184) interprets various human-readable date formats including "Jan-Mar 2021", "2020-2021", and "2022 onwards". It returns concrete `startDate` and `endDate` strings in the format required by JSON-Resume, handling edge cases like "Present" and partial year specifications.

## Implementation Examples

```python
from transform import transform_parsed_data

# Example of a loose LLM output (only a subset of fields)

loose_json = {
    "name": "Jane Doe",
    "email": "jane@example.com",
    "work": [
        {
            "position": "Software Engineer",
            "name": "Acme Corp",
            "startDate": "Jan‑Mar 2020",
            "endDate": "Present",
            "description": ["Built APIs", "Improved performance"]
        }
    ],
    "skills": ["Python", "SQL", "Docker"],
    "education": [
        {"degree": "B.Sc., Computer Science", "institution": "MIT", "years": "2015‑2019"}
    ]
}

resume_json = transform_parsed_data(loose_json)

# `resume_json` now conforms to JSON‑Resume:

# {

#   "basics": {"name": "Jane Doe", "email": "jane@example.com", ...},

#   "work": [

#       {"name": "Acme Corp", "position": "Software Engineer",

#        "startDate": "Jan 2020", "endDate": "Present",

#        "summary": "Built APIs Improved performance", "highlights": []}

#   ],

#   "skills": [

#       {"name": "Programming Languages", "level": null,

#        "keywords": ["Python", "SQL", "Docker"]}

#   ],

#   "education": [

#       {"institution": "MIT", "studyType": "B.Sc.", "area": "Computer Science",

#        "startDate": "2015-01", "endDate": "2019-12", "score": null, "courses": []}

#   ]

# }

```

```python

# Using the same function with a richer input that already contains a `basics` block

loose_json2 = {
    "basics": {
        "name": "John Smith",
        "email": "john@smith.io",
        "profiles": [{"url": "https://github.com/johnsmith"}]
    },
    "projectsOpenSource": [
        {"name": "my-tool | Python, Click", "url": "https://github.com/johnsmith/my-tool"}
    ]
}

resume_json2 = transform_parsed_data(loose_json2)

# The `profiles` entry now has `network: "GitHub"` and `username: "johnsmith"`

# The project is normalised with `technologies` = ["Python", "Click"] and `skills` extracted.

```

## Summary

- **`transform_parsed_data`** serves as the central router, detecting whether input contains full resume data or single sections.
- **Specialized transformers** handle each JSON-Resume section: `transform_basics` for personal info, `transform_work_experience` for jobs, `transform_education` for degrees, and `transform_skills_comprehensive` for technical skills.
- **`parse_date_range`** standardizes human-readable date strings into ISO format, handling ranges like "Jan-Mar 2021" and "Present".
- The module handles **ambiguous LLM outputs** by inferring missing fields, splitting composite strings, and wrapping flat lists into categorized objects.
- All transformations align with the **JSON-Resume specification** as defined in the project's [`models.py`](https://github.com/interviewstreet/hiring-agent/blob/main/models.py), ensuring compatibility with downstream evaluation and export tools.

## Frequently Asked Questions

### What is the main entry point for JSON normalization in transform.py?

The **`transform_parsed_data`** function serves as the main entry point, located at lines 6-87 in [`transform.py`](https://github.com/interviewstreet/hiring-agent/blob/main/transform.py). It detects the structure of incoming LLM JSON and routes it to appropriate specialized transformers or builds a minimal document for single-section inputs.

### How does transform.py handle incomplete LLM JSON with only one section?

When the input contains only a single top-level section (such as only `work` or only `skills`), the function uses the fallback branch at lines 40-87 to create a minimal JSON-Resume document containing just that normalized section, rather than expecting a complete resume structure.

### What date formats can the parse_date_range utility handle?

The **`parse_date_range`** function (lines 112-184) handles human-readable formats including month ranges like "Jan-Mar 2021", year ranges like "2020-2021", ongoing periods marked as "2022 onwards" or "Present", and converts these into standardized start and end date strings suitable for JSON-Resume.

### How are skills categorized when the LLM returns a flat list?

When `skills` arrives as a flat list of strings, **`transform_skills_comprehensive`** (lines 47-75) automatically wraps them under a default category named "Programming Languages". If the input already contains categorized objects, it processes them through `transform_skills` to ensure proper `name`, `level`, and `keywords` structure.