NodeでXML → JSON/CSV変換パッケージを作った話

個人的に作ってるアプリで、[nintendo-switch-eshop](https://www.npmjs.com/package/nintendo-switch-eshop)を使っていました。
その中で、発見があってパッケージよりXMLからの方がデータを多く取れるんですよね実は。（メーカー情報とか取れて便利）
そこでJSONとCSVに変換したいと思って作りました。
(探している中で特になかったんですよね、~~XML久しぶりに見ましたがあんま需要ないのかな~~（これは燃える）)

とりあえず**JSON**と**CSV**がいいかなと思って作りました。

## 格納先
- 作成したGitHubリポジトリ: https://github.com/db-r-hashimoto/xml2json-csv
- 作成したnpmパッケージ: https://www.npmjs.com/package/xml2json-csv

## パッケージの主な特徴

- XML to JSON/CSVの変換
- ネストされたオブジェクトのフラット化
- カスタマイズ可能な変換オプション

## インストール

```bash
npm install xml2json-csv
```

## 使用例
### XML to JSON
```TypeScript
import { xmlToJson } from 'xml2json-csv';

const xmlString = `
  <root>
    <item>
      <name>山田太郎</name>
      <age>30</age>
    </item>
  </root>
`;

async function convertExample() {
  try {
    // 基本的な変換
    const jsonData = await xmlToJson(xmlString);
    console.log(jsonData);

    // Specify custom root element
    const customRootData = await xmlToJson(xmlString, {
      rootElement: "<custom root element>",
    });
    console.log(customRootData);
  } catch (error) {
    console.error('変換エラー:', error);
  }
}
```

### XML to CSV
```TypeScript
import { xmlToCsv } from 'xml2json-csv';

const xmlString = `
  <root>
    <item>
      <name>山田太郎</name>
      <age>30</age>
    </item>
  </root>
`;

async function convertExample() {
  try {
    // 基本的な変換
    const csvData = await xmlToCsv(xmlString);
    console.log(csvData);

    // カスタムCSVオプション
    const customCsv = await xmlToCsv(xmlString, {
      csvOptions: {
        delimiter: ';'
      }
    });
    console.log(customCsv);
  } catch (error) {
    console.error('変換エラー:', error);
  }
}
```

## 実装上の工夫
- カスタムエレメントの実装
XMLの自在生を考えたときに自動で判別できるのが一番ですがうまくいかなかったので、指定できるようにしてあります（parserでうまくやればいいのかな）
```TypeScript
function xmlToJson(
  xmlString: string,
  options: XmlToJsonOptions = {}
)
```

- ネストされた構造の柔軟な処理
ネストは特に厄介でしたね、XML自体がかなり自在にデータを持てるので面倒だった、、、笑
再帰関数を使用して、複雑にネストされたXMLデータを効率的に処理しました。

```TypeScript
function normalizeStructure(
  obj: any,
  options: { arrayHandling: "preserve" | "concatenate"; arraySeparator: string }
): GenericObject | null {
  if (!obj || typeof obj !== "object") return null;

  const result: GenericObject = {};

  for (const [key, value] of Object.entries(obj)) {
    if (!value) continue;

    if (key === "@" && typeof value === "object") {
      // 属性を平坦化
      Object.entries(value as Record<string, unknown>).forEach(
        ([attrKey, attrValue]) => {
          if (attrValue !== null && attrValue !== undefined) {
            result[`${key}${attrKey}`] = attrValue;
          }
        }
      );
    } else if (Array.isArray(value)) {
      // 配列の処理
      const filteredArray = value.filter(
        (v) => v !== null && v !== undefined && v !== ""
      );
      if (filteredArray.length > 0) {
        if (options.arrayHandling === "concatenate") {
          result[key] = `[${filteredArray.join(options.arraySeparator)}]`;
        } else {
          result[key] = filteredArray
            .map((item) =>
              typeof item === "object"
                ? normalizeStructure(item, options)
                : item
            )
            .filter((item): item is GenericObject => item !== null);
        }
      }
    } else if (typeof value === "object") {
      const normalized = normalizeStructure(value, options);
      if (normalized && Object.keys(normalized).length > 0) {
        result[key] = normalized;
      }
    } else if (value !== "") {
      result[key] = value;
    }
  }

  return Object.keys(result).length > 0 ? result : null;
}
```

## 終わりに
XML自体久しく使っていなかったのでちょっと苦戦しました
よかったら使ってみてください！!
あとはGitHubでコード公開してるので、ダメ出しなどいただけたら嬉しいです（Mじゃない）