diff --git a/components/seo/XmlToJsonSEO.tsx b/components/seo/XmlToJsonSEO.tsx new file mode 100644 index 0000000..e818233 --- /dev/null +++ b/components/seo/XmlToJsonSEO.tsx @@ -0,0 +1,207 @@ +import Link from "next/link"; + +export default function XmlToJsonSEO() { + return ( +
+
+

+ Our free, open-source, and ad-free XML to JSON converter makes it easy + to transform your data formats. Convert configuration files, API + responses, or legacy XML data into modern JSON with just a few clicks. +

+
+ +
+

Why Convert XML to JSON?

+

+ XML (eXtensible Markup Language) has been a standard for data exchange + for decades, but JSON (JavaScript Object Notation) has become the + preferred format for modern web development. Converting{" "} + XML to JSON is essential when you need: +

+ +
+ +
+

How to Use Our XML to JSON Converter

+

Converting XML data to JSON has never been easier:

+ +
+ +
+

Key Features of Our XML to JSON Tool

+ +
+ +
+

XML vs JSON: When to Use Each

+

Both XML and JSON have their strengths. Here's when to use each:

+ +
+ +
+

Understanding the Conversion Format

+

Our converter uses industry-standard conventions:

+ +
+ +
+

FAQs

+ +
+ +
+

Related Tools

+

Check out our other data conversion utilities:

+ +
+
+ ); +} diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts index a5ac9ba..343bccb 100644 --- a/components/utils/tools-list.ts +++ b/components/utils/tools-list.ts @@ -167,4 +167,10 @@ export const tools = [ "Check color contrast ratios for WCAG AA and AAA compliance. Ensure your designs meet accessibility standards with our free color contrast checker tool.", link: "/utilities/wcag-color-contrast-checker", }, + { + title: "XML to JSON", + description: + "Transform XML data into JSON format instantly. Simplifies working with APIs and modern web applications that prefer JSON.", + link: "/utilities/xml-to-json", + }, ]; diff --git a/components/utils/xml-to-json.test.ts b/components/utils/xml-to-json.test.ts new file mode 100644 index 0000000..0be15b3 --- /dev/null +++ b/components/utils/xml-to-json.test.ts @@ -0,0 +1,632 @@ +import { xmlToJson } from "./xml-to-json.utils"; + +describe("XML to JSON Converter", () => { + describe("Basic conversion", () => { + it("should convert simple XML to JSON", () => { + const xml = "test"; + const result = xmlToJson(xml); + expect(result).toEqual({ root: { name: "test" } }); + }); + + it("should handle nested elements", () => { + const xml = "value"; + const result = xmlToJson(xml); + expect(result).toEqual({ + root: { parent: { child: "value" } }, + }); + }); + + it("should handle empty elements", () => { + const xml = ""; + const result = xmlToJson(xml); + expect(result).toEqual({ root: { empty: null } }); + }); + }); + + describe("Attribute handling", () => { + it("should convert XML attributes to @attributes object", () => { + const xml = 'test'; + const result = xmlToJson(xml); + expect(result).toEqual({ + root: { "@attributes": { id: "123" }, name: "test" }, + }); + }); + + it("should handle multiple attributes", () => { + const xml = ''; + const result = xmlToJson(xml); + expect(result).toEqual({ + root: { + "@attributes": { id: "123", class: "main", "data-test": "true" }, + }, + }); + }); + + it("should handle element with only attributes", () => { + const xml = ''; + const result = xmlToJson(xml); + expect(result).toEqual({ + root: { "@attributes": { id: "123" } }, + }); + }); + + it("should handle self-closing tags with attributes", () => { + const xml = ''; + const result = xmlToJson(xml); + expect(result).toEqual({ + root: { + coordinates: { + "@attributes": { + lat: "40.7128", + lon: "-74.0060", + }, + }, + }, + }); + }); + }); + + describe("Array handling", () => { + it("should convert multiple same-named elements to array", () => { + const xml = "123"; + const result = xmlToJson(xml); + expect(result).toEqual({ root: { item: ["1", "2", "3"] } }); + }); + + it("should handle mixed elements with arrays", () => { + const xml = "test12"; + const result = xmlToJson(xml); + expect(result).toEqual({ + root: { name: "test", item: ["1", "2"] }, + }); + }); + + it("should handle arrays of objects with attributes", () => { + const xml = + 'firstsecond'; + const result = xmlToJson(xml); + expect(result).toEqual({ + root: { + item: [ + { "#text": "first", "@attributes": { id: "1" } }, + { "#text": "second", "@attributes": { id: "2" } }, + ], + }, + }); + }); + }); + + describe("Text content handling", () => { + it("should handle text content with attributes using #text", () => { + const xml = 'text content'; + const result = xmlToJson(xml); + expect(result).toEqual({ + root: { "#text": "text content", "@attributes": { id: "1" } }, + }); + }); + + it("should trim whitespace from text content", () => { + const xml = " test "; + const result = xmlToJson(xml); + expect(result).toEqual({ root: { name: "test" } }); + }); + + it("should handle mixed content (text + elements)", () => { + const xml = '12.50'; + const result = xmlToJson(xml); + expect(result).toEqual({ + price: { "#text": "12.50", "@attributes": { currency: "USD" } }, + }); + }); + }); + + describe("Error handling", () => { + it("should throw error for invalid XML", () => { + const xml = ""; + expect(() => xmlToJson(xml)).toThrow("Invalid XML"); + }); + + it("should throw error for malformed XML", () => { + const xml = "not xml at all"; + expect(() => xmlToJson(xml)).toThrow("Invalid XML"); + }); + }); + + describe("Complex nested structure", () => { + it("should convert complex library XML structure", () => { + const xml = ` + + + City Central Library + + New York + USA + + + 1832 + + + + + The Great Gatsby + + F. Scott + Fitzgerald + + 978-0-7432-7356-5 + 1925 + + Fiction + Classic + + 12.50 + 5 + A novel set in the Jazz Age that explores themes of decadence, idealism, and excess. + + + + 1984 + + George + Orwell + + 978-0-452-28423-4 + 1949 + + Dystopian + Science Fiction + Political Fiction + + 9.80 + 0 + A dystopian social science fiction novel and cautionary tale. + + + + To Kill a Mockingbird + + Harper + Lee + + 978-0-06-112008-4 + 1960 + + Fiction + Classic + Historical + + 11.00 + 3 + + Pulitzer Prize + Brotherhood Award + + A novel about racial injustice and childhood innocence in the American South. + + + + + + John Doe + john.doe@example.com + +1 555 123 4567 + 2024-03-15 + + + + + + + Jane Smith + jane.smith@example.com + +1 555 987 6543 + 2023-07-22 + + + + + + + + Bob Johnson + bob.johnson@example.com + +1 555 555 7890 + 2022-11-10 + + Late returns + + + + + + Alice Williams + Reference + 2018-05-01 + + + + + + + + + Charlie Brown + Administration + 2015-02-15 + + + + + + + + + + + + + Creative Writing Workshop + 2026-02-10 + + David Miller + 15 + + + + Poetry Night + 2026-01-25 + + + Emily Davis + Frank Wilson + + 42 + + + + + + 14 + 5 + 0.50 + + + + + + + +`; + + const result = xmlToJson(xml); + + expect(result).toEqual({ + library: { + metadata: { + name: "City Central Library", + location: { + city: "New York", + country: "USA", + coordinates: { + "@attributes": { + lat: "40.7128", + lon: "-74.0060", + }, + }, + }, + established: "1832", + }, + books: { + book: [ + { + "@attributes": { + id: "001", + available: "true", + }, + title: "The Great Gatsby", + author: { + "@attributes": { + nationality: "American", + }, + firstName: "F. Scott", + lastName: "Fitzgerald", + }, + isbn: "978-0-7432-7356-5", + published: "1925", + genres: { + genre: ["Fiction", "Classic"], + }, + price: { + "#text": "12.50", + "@attributes": { + currency: "USD", + }, + }, + copies: "5", + description: + "A novel set in the Jazz Age that explores themes of decadence, idealism, and excess.", + }, + { + "@attributes": { + id: "002", + available: "false", + }, + title: "1984", + author: { + "@attributes": { + nationality: "British", + }, + firstName: "George", + lastName: "Orwell", + }, + isbn: "978-0-452-28423-4", + published: "1949", + genres: { + genre: ["Dystopian", "Science Fiction", "Political Fiction"], + }, + price: { + "#text": "9.80", + "@attributes": { + currency: "USD", + }, + }, + copies: "0", + description: + "A dystopian social science fiction novel and cautionary tale.", + }, + { + "@attributes": { + id: "003", + available: "true", + }, + title: "To Kill a Mockingbird", + author: { + "@attributes": { + nationality: "American", + }, + firstName: "Harper", + lastName: "Lee", + }, + isbn: "978-0-06-112008-4", + published: "1960", + genres: { + genre: ["Fiction", "Classic", "Historical"], + }, + price: { + "#text": "11.00", + "@attributes": { + currency: "USD", + }, + }, + copies: "3", + awards: { + award: [ + { + "#text": "Pulitzer Prize", + "@attributes": { + year: "1961", + }, + }, + { + "#text": "Brotherhood Award", + "@attributes": { + year: "1961", + }, + }, + ], + }, + description: + "A novel about racial injustice and childhood innocence in the American South.", + }, + ], + }, + members: { + member: [ + { + "@attributes": { + id: "M001", + status: "active", + }, + name: "John Doe", + email: "john.doe@example.com", + phone: "+1 555 123 4567", + joinDate: "2024-03-15", + borrowedBooks: { + bookRef: { + "@attributes": { + id: "001", + dueDate: "2026-01-20", + }, + }, + }, + }, + { + "@attributes": { + id: "M002", + status: "active", + }, + name: "Jane Smith", + email: "jane.smith@example.com", + phone: "+1 555 987 6543", + joinDate: "2023-07-22", + borrowedBooks: { + bookRef: [ + { + "@attributes": { + id: "003", + dueDate: "2026-01-15", + }, + }, + { + "@attributes": { + id: "001", + dueDate: "2026-01-18", + }, + }, + ], + }, + }, + { + "@attributes": { + id: "M003", + status: "suspended", + }, + name: "Bob Johnson", + email: "bob.johnson@example.com", + phone: "+1 555 555 7890", + joinDate: "2022-11-10", + borrowedBooks: null, + suspensionReason: "Late returns", + }, + ], + }, + staff: { + employee: [ + { + "@attributes": { + id: "E001", + role: "librarian", + }, + name: "Alice Williams", + department: "Reference", + hireDate: "2018-05-01", + schedule: { + shift: [ + { + "@attributes": { + day: "Monday", + start: "09:00", + end: "17:00", + }, + }, + { + "@attributes": { + day: "Wednesday", + start: "09:00", + end: "17:00", + }, + }, + { + "@attributes": { + day: "Friday", + start: "09:00", + end: "17:00", + }, + }, + ], + }, + }, + { + "@attributes": { + id: "E002", + role: "manager", + }, + name: "Charlie Brown", + department: "Administration", + hireDate: "2015-02-15", + schedule: { + shift: [ + { + "@attributes": { + day: "Monday", + start: "08:00", + end: "16:00", + }, + }, + { + "@attributes": { + day: "Tuesday", + start: "08:00", + end: "16:00", + }, + }, + { + "@attributes": { + day: "Wednesday", + start: "08:00", + end: "16:00", + }, + }, + { + "@attributes": { + day: "Thursday", + start: "08:00", + end: "16:00", + }, + }, + { + "@attributes": { + day: "Friday", + start: "08:00", + end: "16:00", + }, + }, + ], + }, + }, + ], + }, + events: { + event: [ + { + "@attributes": { + type: "workshop", + capacity: "20", + }, + title: "Creative Writing Workshop", + date: "2026-02-10", + time: "18:00", + instructor: "David Miller", + registrations: "15", + }, + { + "@attributes": { + type: "reading", + capacity: "50", + }, + title: "Poetry Night", + date: "2026-01-25", + time: "19:30", + guests: { + guest: ["Emily Davis", "Frank Wilson"], + }, + registrations: "42", + }, + ], + }, + config: { + settings: { + maxBorrowDays: "14", + maxBooksPerMember: "5", + lateFeePerDay: { + "#text": "0.50", + "@attributes": { + currency: "USD", + }, + }, + openingHours: { + weekday: { + "@attributes": { + open: "08:00", + close: "20:00", + }, + }, + saturday: { + "@attributes": { + open: "09:00", + close: "15:00", + }, + }, + sunday: { + "@attributes": { + closed: "true", + }, + }, + }, + }, + }, + }, + }); + }); + }); +}); diff --git a/components/utils/xml-to-json.utils.ts b/components/utils/xml-to-json.utils.ts new file mode 100644 index 0000000..d778193 --- /dev/null +++ b/components/utils/xml-to-json.utils.ts @@ -0,0 +1,99 @@ +/** + * Converts XML string to JSON object + * @param xml - The XML string to convert + * @returns The parsed JSON object + */ +export function xmlToJson(xml: string): unknown { + const parser = new DOMParser(); + const doc = parser.parseFromString(xml, "text/xml"); + + // Check for parsing errors + const parserError = doc.querySelector("parsererror"); + if (parserError) { + throw new Error("Invalid XML"); + } + + function nodeToJson(node: Node): unknown { + const obj: Record = {}; + + // Handle element nodes + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + + // Handle child nodes first + const children: Record = {}; + let hasTextContent = false; + let textContent = ""; + + if (element.hasChildNodes()) { + for (let i = 0; i < element.childNodes.length; i++) { + const child = element.childNodes[i]; + + // Text node + if ( + child.nodeType === Node.TEXT_NODE || + child.nodeType === Node.CDATA_SECTION_NODE + ) { + const text = child.textContent?.trim(); + if (text) { + hasTextContent = true; + textContent = text; + } + } + // Element node + else if (child.nodeType === Node.ELEMENT_NODE) { + const childName = child.nodeName; + const childValue = nodeToJson(child); + + if (!children[childName]) { + children[childName] = []; + } + children[childName].push(childValue); + } + } + } + + // Add child elements to object + for (const [name, values] of Object.entries(children)) { + if (values.length === 1) { + obj[name] = values[0]; + } else { + obj[name] = values; + } + } + + // Handle text content + if (hasTextContent) { + // If only text content and no attributes, return text directly + if (Object.keys(obj).length === 0 && element.attributes.length === 0) { + return textContent; + } + // If has attributes or children, add as #text + if (element.attributes.length > 0 || Object.keys(obj).length > 0) { + obj["#text"] = textContent; + } + } + + // Add attributes as @attributes object + if (element.attributes.length > 0) { + const attributes: Record = {}; + for (let i = 0; i < element.attributes.length; i++) { + const attr = element.attributes[i]; + attributes[attr.nodeName] = attr.nodeValue || ""; + } + obj["@attributes"] = attributes; + } + + // If empty element, return null + const keys = Object.keys(obj); + if (keys.length === 0) { + return null; + } + } + + return obj; + } + + const root = doc.documentElement; + return { [root.nodeName]: nodeToJson(root) }; +} diff --git a/pages/utilities/xml-to-json.tsx b/pages/utilities/xml-to-json.tsx new file mode 100644 index 0000000..abd301b --- /dev/null +++ b/pages/utilities/xml-to-json.tsx @@ -0,0 +1,92 @@ +import CallToActionGrid from "@/components/CallToActionGrid"; +import { CMDK } from "@/components/CMDK"; +import { Button } from "@/components/ds/ButtonComponent"; +import { Card } from "@/components/ds/CardComponent"; +import { Label } from "@/components/ds/LabelComponent"; +import { Textarea } from "@/components/ds/TextareaComponent"; +import GitHubContribution from "@/components/GitHubContribution"; +import Header from "@/components/Header"; +import { useCopyToClipboard } from "@/components/hooks/useCopyToClipboard"; +import PageHeader from "@/components/PageHeader"; +import XmlToJsonSEO from "@/components/seo/XmlToJsonSEO"; +import { xmlToJson } from "@/components/utils/xml-to-json.utils"; +import { useCallback, useState } from "react"; +import Meta from "../../components/Meta"; + +export default function XMLtoJSON() { + const [input, setInput] = useState(""); + const [output, setOutput] = useState(""); + const { buttonText, handleCopy } = useCopyToClipboard(); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const { value } = event.currentTarget; + setInput(value); + + if (!value.trim()) { + setOutput(""); + return; + } + + try { + const jsonObject = xmlToJson(value.trim()); + const output = JSON.stringify(jsonObject, null, 2); + setOutput(output); + } catch { + setOutput("Invalid XML input"); + } + }, + [] + ); + + return ( +
+ +
+ + +
+ +
+ +
+ +
+ +