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

The 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 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.

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.

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

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": []}

#   ]

# }

# 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, 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →