fix(import): auto-detect JSON format and show readable errors (#1) (#3296)

* fix(import): auto-detect JSON format and show readable errors (#1)

Readable import errors, a fail-soft v4 parser, and auto-detect of the JSON format so uploads just work. The format dropdown becomes an optional override. PDF and DOCX (AI) paths are untouched.

* fix(import): address review feedback on the v4 guard and error message

- reactive-resume-v4-json.tsx: reject arrays in isRecord so array-valued
  basics, sections, or metadata no longer pass the v4 shape guard.
- reactive-resume-v4-json.tsx: reuse the guard's error instance in the
  catch arm instead of allocating a duplicate NOT_V4_MESSAGE.
- error.ts: use a singular "Problem" label for root-level Zod issues so
  the message stays grammatical.
- add a regression test for array-valued v4 branches.

* fix(import): preserve selected JSON format

* test(import): cover selected JSON parser

---------

Co-authored-by: MrTig-afk <MrTig-afk@users.noreply.github.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
Kaushik N
2026-08-13 23:01:06 +02:00
committed by GitHub
co-authored by MrTig-afk Amruth Pillai
parent 9f13638eab
commit bad431b2fc
8 changed files with 139 additions and 18 deletions
@@ -19,6 +19,7 @@ describe("detectJsonImportType", () => {
});
it("returns an empty string for unrecognized shapes", () => {
expect(detectJsonImportType({})).toBe("");
expect(detectJsonImportType({ foo: "bar" })).toBe("");
expect(detectJsonImportType(null)).toBe("");
expect(detectJsonImportType("nope")).toBe("");
+8 -13
View File
@@ -1,6 +1,7 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { DialogProps } from "../store";
import type { ImportType } from "./import.utils";
import type { ResumeJsonFormat } from "./parse-json";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { DownloadSimpleIcon, FileIcon, UploadSimpleIcon } from "@phosphor-icons/react";
@@ -10,9 +11,6 @@ import { Link, useNavigate } from "@tanstack/react-router";
import { useRef, useState } from "react";
import { toast } from "sonner";
import z from "zod";
import { parseJSONResume } from "@reactive-resume/import/json-resume";
import { parseReactiveResumeJSON } from "@reactive-resume/import/reactive-resume-json";
import { parseReactiveResumeV4JSON } from "@reactive-resume/import/reactive-resume-v4-json";
import { Badge } from "@reactive-resume/ui/components/badge";
import { Button } from "@reactive-resume/ui/components/button";
import {
@@ -34,6 +32,7 @@ import { client, orpc } from "@/libs/orpc/client";
import { useAppForm } from "@/libs/tanstack-form";
import { useDialogStore } from "../store";
import { detectJsonImportType } from "./import.utils";
import { parseResumeJson } from "./parse-json";
const formSchema = z.discriminatedUnion("type", [
z.object({
@@ -149,16 +148,12 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
try {
let data: ResumeData | undefined;
if (value.type === "json-resume-json") {
data = parseJSONResume(await value.file.text());
}
if (value.type === "reactive-resume-json") {
data = parseReactiveResumeJSON(await value.file.text());
}
if (value.type === "reactive-resume-v4-json") {
data = parseReactiveResumeV4JSON(await value.file.text());
if (
value.type === "json-resume-json" ||
value.type === "reactive-resume-json" ||
value.type === "reactive-resume-v4-json"
) {
data = parseResumeJson(await value.file.text(), value.type as ResumeJsonFormat);
}
if (value.type === "pdf") {
@@ -0,0 +1,8 @@
import { describe, expect, it } from "vitest";
import { parseResumeJson } from "./parse-json";
describe("parseResumeJson", () => {
it("keeps a selected v4 import from falling back to JSON Resume", () => {
expect(() => parseResumeJson("{}", "reactive-resume-v4-json")).toThrow(/v4/i);
});
});
+12
View File
@@ -0,0 +1,12 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { parseJSONResume } from "@reactive-resume/import/json-resume";
import { parseReactiveResumeJSON } from "@reactive-resume/import/reactive-resume-json";
import { parseReactiveResumeV4JSON } from "@reactive-resume/import/reactive-resume-v4-json";
export type ResumeJsonFormat = "reactive-resume-json" | "reactive-resume-v4-json" | "json-resume-json";
export function parseResumeJson(text: string, format: ResumeJsonFormat): ResumeData {
if (format === "reactive-resume-json") return parseReactiveResumeJSON(text);
if (format === "reactive-resume-v4-json") return parseReactiveResumeV4JSON(text);
return parseJSONResume(text);
}