Interactive demo · synthetic dataEvery record on this page is fabricated.

Extraction Without a Schema

Records recognised by shape, not by path, from a GraphQL API that is reshaped without notice.

The production code behind this demo is my own work — design, implementation.

Read about the system

THE RESPONSE

NOW BREAK IT

Apply one, two or all three. The response changes underneath the extractor; the records it returns do not.

THE RESPONSE AS RECEIVED · 2,011 CHARS

{
  "data": {
    "jobAd": {
      "__typename": "JobAd",
      "id": "job-4410",
      "title": "Warehouse Supervisor",
      "reference": "SN-WH-4410",
      "location": {
        "label": "Bandung, West Java"
      },
      "postedAt": "2026-02-02",
      "expiryDate": "2026-03-04",
      "applicationCount": 3,
      "applications": {
        "edges": [
          {
            "node": {
              "__typename": "Candidate",
              "id": "cand-8871",
              "fullName": "Marisol Adeyemi-Okonkwo",
              "email": "m.adeyemi@example.invalid",
              "phone": "+62 811 555 04",
              "appliedDateTime": "2026-02-11T09:12:00+07:00",
              "statusInfo": {
                "label": "Shortlisted"
              },
              "attachments": [
                {
                  "__typename": "CandidateAttachment",
                  "url": "https://files.example.invalid/a/9f2c.pdf",
                  "fileName": "Marisol_Adeyemi_CV.pdf",
                  "type": "RESUME"
                },
                {
                  "__typename": "CandidateAttachment",
                  "url": "https://files.example.invalid/a/9f2d.docx",
                  "fileName": "cover letter.docx",
                  "type": "COVER_LETTER"
                }
              ]
            }
          },
          {
            "node": {
              "__typename": "Candidate",
              "id": "cand-8872",
              "fullName": "Ignatius Broome",
              "email": "i.broome@example.invalid",
              "phone": "+62 811 555 05",
              "appliedDateTime": "2026-02-12T14:40:00+07:00",
              "applicationStatus": "Interviewing",
              "attachments": [
                {
                  "__typename": "CandidateAttachment",
                  "url": "https://files.example.invalid/a/1a04.pdf",
                  "fileName": "broome-cv-2026.pdf"
                }
              ]
            }
          }
        ]
      }
    }
  }
}

EXTRACT.TS · WALKS THE SHAPE

1JOBS

2CANDIDATES

3DOCUMENTS

Recognised by shape, not by path: a name, an id, and a signal that this is a person who applied for something.

NAIVEEXTRACT · HARD-CODES THE PATH

2CANDIDATES

3DOCUMENTS

root.data.jobAd.applications.edges

The path resolved. This is the version almost everyone writes first, and it is correct until the morning the vendor renames a key.

  • Marisol Adeyemi-Okonkwo
  • Ignatius Broome

WHAT CAME BACK

  • Warehouse Supervisor

    job-4410 · SN-WH-4410 · Bandung, West Java · CLOSES 2026-03-04 · 3 APPLICATIONS · JobAd

CANDIDATES
NAMEIDCONTACTAPPLIEDSTATUSJOB
Marisol Adeyemi-Okonkwocand-8871m.adeyemi@example.invalid+62 811 555 042026-02-11Shortlistedjob-4410
Ignatius Broomecand-8872i.broome@example.invalid+62 811 555 052026-02-12Interviewingjob-4410
DOCUMENTS
FILEKINDREADS ASATTRIBUTED TO
Marisol_Adeyemi_CV.pdfhttps://files.example.invalid/a/9f2c.pdfCandidateAttachment RESUMECVcand-8871job-4410
cover letter.docxhttps://files.example.invalid/a/9f2d.docxCandidateAttachment COVER_LETTERCOVER LETTERcand-8871job-4410
broome-cv-2026.pdfhttps://files.example.invalid/a/1a04.pdfCandidateAttachmentCVcand-8872job-4410

P1 · THE ORDINARY CASE

The shape the portal returns most days. Candidates are nested inside the job and their documents are nested inside them, so every record comes out fully attributed: each CV knows which person it belongs to and which job they applied for. Nothing in the extractor knows this path exists — it is inferred from the nesting.

WHY THE TOOL EXISTS, AND ITS LIMITS

  • The portal deletes applicant attachments 180 days after an ad closes and has no bulk download. That constraint is the first thing the README states.
  • The password is typed by the human into a real browser. The script never sees or stores a credential; only the resulting session file, which is deleted after.
  • This is real people's personal data. Store it access-controlled, keep it only as long as hiring needs it.
  • If you need this supported rather than automated, the vendor sells an official partner application-export API. Use that instead.
What is real, and what is simulated

Runs in your browser

  • extract.ts — the 200-line production extractor, ported verbatim: the recursive walk, the key-preference lists, both regexes and every guard
  • looksLikeCandidate / looksLikeJob / looksLikeAttachment — the shape tests unchanged, including the rejections that throw out a company, an account, a location or a currency
  • Attribution by nesting: an attachment inherits the candidate it was found inside, and that candidate inherits the job
  • The merge by id that lets three partial responses fill in one record, the 40-level depth guard, and the WeakSet that stops a cycle spinning

extract.parity.test.ts rebuilds a 105,861-case matrix — 105,840 combinations of name, id, signal, contact, attachment and nesting shapes, plus 21 hand-written edge roots — and compares a SHA-256 of every result against a digest generated by running the original extractor in its own repository. It runs in npm run verify, so a widened character class or a reordered key list fails the build.

Simulated

  • The browser automation around it: the sign-in the human performs, the saved session, the walk across every job ad and applicant
  • Downloading a CV on sight from a short-lived signed URL, the per-job folders and the CSV index it writes
  • The three mutation buttons and the hard-coded-path comparison — both written for this page, neither is production code
  • Every payload, applicant, employer, file and address on this page. The hosts end in .invalid and can never resolve