Loading config files from an API in either JSON or XML format

react typescript json xml fetch

For a project I've been working on for a client an interesting problem came up lately.

I was tasked with creating a 3D file viewer that gets its input from either a json or an xml config file. My initial idea was to use 2 props on the component jsonConfig and xmlConfig, but that would cause all sorts of other issues:

  • How could I make one of both props required ?
<MyComponent
    xmlConfig={} /** required if no jsonConfig is given */
    jsonConfig={} /** required if no xmlConfig is given */
/>
  • Clearly the devs that would need to integrate my component into the framework would need to be aware of this duality and that could potentially cause issues later on.

I clearly wasn't going to take that approach. Instead I decided to define a single configPath prop and figure out the content based on the file extension.

<MyComponent
    configPath={} /** pass either json or xml config, we'll take it from there :rocket: */
/>
const [json, setJson] = (useState < ConfigType) | (null > null);

useEffect(() => {
    const fetchData = async () => {
        const extension = configPath.split(".").pop();

        try {
            const response = await fetch(configPath);

            switch (extension) {
                case "xml":
                    const text = await response.text();
                    setJson(
                        // xmlToJson is a custom function that maps the xml Dom structure to a valid typed Config struct.
                        xmlToJson(
                            new DOMParser().parseFromString(text, "text/xml")
                        )
                    );
                    break;
                case "json":
                    const jsonData = await response.json();
                    setJson(jsonData);
                    break;

                default:
                    console.error(
                        `Unknown extension (.${extension}), please use .xml or .json`
                    );
                    break;
            }
        } catch (error) {
            console.error(error);
            setJson(null);
        }
    };

    // fetch
    fetchData();
}, [configPath]);

This worked fine during development, all my stories worked fine so I was happy with this approach.

It was not until the component was ready for integration into the rest of the framework that my obviously naive approach about the file extension came to an abrupt realisation. When the real path to the API was given to the configPath prop nothing happened, no data was loaded because the API just returned the data, there was no filename, least of all: no file extension !

<MyComponent configPath="https://website.com/api/f21be9b7-642e-4d9a-a334-aa40592b7393" />

This forced me to revisit my fetch hook and find some means of inspecting the data that was coming in to see wheather it was in fact JSON or XML ...

The best I could come up with was another try catch block.

I'd first attempt to parse the data blindly expecting it to be JSON, if that wouldn't work I'd fall back parsing it as XML. If that wouldn't work either I'd still have my first try/catch block to show an error.

try {
    // Fetch data file from server
    const response = await fetch(configPath);

    try {
        // Attempt to parse response as JSON
        const jsonData = await response.json();
        setJson(jsonData);
    } catch (error) {
        // Attempt to parse response as XML
        const text = await response.text();
        setJson(xmlToJson(new DOMParser().parseFromString(text, "text/xml")));
    }
} catch (error) {
    console.error(error);
    setJson(null);
}

Now loading json files started to work again but when attempting to load an xml file the following error was thrown:

TypeError: Failed to execute 'text' on 'Response': body stream already read

So it seems the error is trying to say: Hey, I've already read the response (as json) and I can't do it again. Luckily there is an easy fix to this problem using the Reponse.clone() API.

So we end up with this:

try {
    // Attempt to parse response as JSON
    const jsonData = await response.clone().json();
    setJson(jsonData);
} catch (error) {
    // Attempt to parse response as XML
    const text = await response.clone().text();
    setJson(xmlToJson(new DOMParser().parseFromString(text, "text/xml")));
}

PS: Converting the XML to JSON using a typed converter is also a very interesting problem to solve. I will try to write about that later.