App-Idea 6: JSON to CSV Converter (Part 1)

The next app that we will look at is the JSON to CSV Converter, which allows users to enter text in a simplified JSON format and converts it into a comma-separated value (CSV) format. To map easily to the flat CSV structure, our JSON input won’t deeply support nested objects. This app-idea has more complex requirements, so we will cover this application in 3 parts:

  • In part 1 (this article), we will focus on a basic conversion functionality to allow users to type in JSON text and convert it to CSV format.
  • In part 2, we will add functionality to copy the converted text to the clipboard, and load the JSON input from a local file.
  • In part 3, we will add functionality to save the converted text as a local CSV file.

In this article, we will use System.Text.Json to parse the input JSON text into a JsonDocument, including handling errors in JSON formats. Then, we will use a valid JsonDocument structure to enumerate and build the corresponding CSV form of the input.

We will add this app as a new page to the existing Blazor.AppIdeas.Converters app (rather than creating a separate full project). You can find the source for the Blazor.AppIdeas.Converters on GitHub. And the running sample of the Blazor app online.

We will start by loading Blazor.AppIdeas.Converters solution (created in App-Idea 1: Bin2Dec) into Visual Studio.

Model Class

We will start with the TextConverter model class. It will encapsulate our ability to perform various text conversions. The first one being to convert JSON text into CSV text format. Let’s create the TextConverter class in the Blazor.AppIdeas.Converters project and Models folder.

using System;
using System.Linq;
using System.Text;
using System.Text.Json;

namespace Blazor.AppIdeas.Converters.Models
{
    public class TextConverter
    {
        public TextConverter(string text)
        {
            SourceText = text ?? throw new ArgumentNullException(nameof(text));
        }

        public string SourceText { get; }

        public string JsonToCsv()
        {
            if (string.IsNullOrEmpty(SourceText)) return string.Empty;

            var result = new StringBuilder();
            var element = GetJsonRootArray();

            result.AppendLine(ParseJsonPropertyNames(element));

            result.Append(ParseJsonElementArray(element));

            return result.ToString();
        }

        private JsonElement GetJsonRootArray()
        {
            var doc = JsonDocument.Parse(SourceText);
            if (doc.RootElement.ValueKind == JsonValueKind.Object)
            {
                doc = JsonDocument.Parse($"[{SourceText}]");
            }

            if (doc.RootElement.ValueKind != JsonValueKind.Array)
            {
                throw new NotSupportedException("Unsupported JSON root type.");
            }

            return doc.RootElement;
        }

        private string ParseJsonPropertyNames(JsonElement root)
        {
            var result = new StringBuilder();
            var firstElement = root.EnumerateArray().First();

            foreach (var property in firstElement.EnumerateObject())
            {
                result.Append($"{property.Name}, ");
            }

            return result.ToString().TrimEnd(',', ' ');
        }

        private string ParseJsonElementArray(JsonElement element)
        {
            StringBuilder result = new StringBuilder();

            foreach (var e in element.EnumerateArray())
            {
                result.AppendLine(ParseJsonElement(e));
            }

            return result.ToString();
        }

        private string ParseJsonElement(JsonElement element)
        {
            var result = new StringBuilder();
            foreach (var property in element.EnumerateObject())
            {
                result.Append($"{property.Value}, ");
            }

            return result.ToString().TrimEnd(',', ' ');
        }
    }
}
  • This class starts with a constructor (lines #10-13) that takes in the source text to convert.
  • We expose a read-only SourceText property for callers to access that data.
  • Then, let’s focus on the private helper methods:
    • The ParseJsonElement method (lines #72-81) takes a JsonElement and produces a comma-separate string for each value. It enumerates all of the properties and appends its value to the resulting string.
    • The ParseJsonElementArray method (lines #60-70) takes a root element and produces the string for all of its child elements. It calls ParseJsonElement on each item in the array and concatenates the results. Returning the full string with each array element on a new line.
    • The ParseJsonPropertyNames method (lines #47-58) uses the first JsonElement in the root to gather a comma-separate string of each property name. These property names are used as the header in the CSV file.
    • This assumes that all of the elements in the list all have the same properties… JSON elements with optional properties may give unexpected results.
    • The GetJsonRootArray method (lines #31-45) parse the SourceText and produces a JsonDocument and its root element. JSON files can be a single object or an array of objects. So if we encounter a file with a single object, we create a single element array and return it as the root. Finally, if the JsonValueKind of the root element is anything other than Object or Array, then we throw a NotSupportedException.
  • With all of the helper methods defined, the main JsonToCsv method (lines #17-29) is easy to understand:
    • If we don’t have a value in SourceText, then we throw an exception.
    • We use GetJsonRootArray to get the root element for the SourceText.
    • We first get the CSV header line with property names by calling ParseJsonPropertyNames on the root element.
    • Then, we get the full set of lines for the CSV body by calling ParseJsonElementArray on the root element.
    • We use StringBuilders to make all of the string concatenation more performant.
    • We allow all exceptions to bubble out to the callers to handle.

This class has all of the responsibility of taking JSON source text and producing a CSV formatted result. If there are nested objects in the JSON, then the nested object data is treated as a string and copied into that value in the CSV line. That seemed like a reasonable solution to handling nested objects for this simple converter.

JsonCsvConverter View Model

Next, let’s create the JsonCsvConverterViewModel class in the Blazor.AppIdeas.Converters project and ViewModels folder.

using Blazor.AppIdeas.Converters.Models;
using System;

namespace Blazor.AppIdeas.Converters.ViewModels
{
    public class JsonCsvConverterViewModel
    {
        public string SourceText { get; set; }

        public string ConvertedText { get; set; }

        public string ErrorMessage { get; private set; }

        public bool HasError => !string.IsNullOrEmpty(ErrorMessage);

        public void ConvertToCsv()
        {
            try
            {
                ErrorMessage = null;

                var converter = new TextConverter(SourceText);
                ConvertedText = converter.JsonToCsv();
            }
            catch (Exception ex)
            {
                ErrorMessage = $"Cannot parse source text. {ex.Message}";
                ConvertedText = null;
            }
        }

        public void ClearAll()
        {
            SourceText = string.Empty;
            ConvertedText = string.Empty;
            ErrorMessage = null;
        }
    }
}

This view model class surfaces the properties and operations used by the view and communicates with the TextConverter model class.

  • The SourceText property to hold the JSON text to convert.
  • The ConvertedText property then holds the converted CSV text.
  • The ErrorText property is used to show a message when an error is processed by the view model.
  • The ConvertToCsv method (lines #16-30) creates a TextConverter with the SourceText and calls JsonToCsv. Storing the returned value in ConvertedText. It also handles any exceptions by showing a specific error message.
  • The ClearAll method (lines #32-37) clears all of the view model’s properties.

JsonCsvConverter Page

Now, let’s create the JsonCsvConverter page in the Blazor.AppIdeas.Converters project and ViewModels folder.

@page "/json-csv-convert"
@inject JsonCsvConverterViewModel vm

<div class="col-12 mt-3 pb-2 mb-3 container">
    <h3 class="mt-3">JSON-CSV Converter</h3>
    <hr />
    <EditForm class="form-row" Model=@vm>
        <div class="form-group col-lg-5 col-md-12">
            <InputTextArea id="convert-from-value" class="form-control"
                           placeholder="Type JSON or CSV..." rows="8"
                           @bind-Value=vm.SourceText />
            <button id="open-file" class="btn btn-outline-secondary form-control">
                <span class="oi oi-cloud-upload" aria-hidden="true" />
                Open Data File
            </button>
        </div>
        <div class="col-lg-2 col-md-12">
            <button id="btn-convert-csv"
                    class="btn btn-primary col-lg-10 offset-lg-1 col-6 mb-lg-1 mb-3"
                    @onclick="vm.ConvertToCsv">
                To CSV
            </button>
            <button id="btn-clear"
                    class="btn btn-primary col-lg-10 offset-lg-1 col-5 mb-lg-1 mb-3"
                    @onclick="vm.ClearAll">
                Clear
            </button>
        </div>
        <div class="form-group col-lg-5 col-12">
            <InputTextArea id="converted-value" class="form-control" rows="8"
                           readonly @bind-Value=vm.ConvertedText />
            <div>
                <button id="download-file" class="btn btn-outline-secondary text-nowrap" style="width: 52%">
                    <span class="oi oi-cloud-download" aria-hidden="true" />
                    Download File
                </button>
                <button id="clipboard-copy" class="btn btn-outline-secondary" style="width: 46.5%; margin-left: -2px">
                    <span class="oi oi-clipboard" aria-hidden="true" />
                    Copy
                </button>
            </div>
        </div>
        @if (vm.HasError)
        {
         <div class="alert alert-danger col-12 ml-1">
             <strong>Error:</strong> @vm.ErrorMessage
         </div>
        }
    </EditForm>
</div>
  • First, we inject the JsonCsvConverterViewModel class into this page (line #2).
  • Then, we define the InputTextArea (lines #9-11) for the source text. We use @bind-Value to two-way bind this component to the JsonCsvConverterViewModel.SourceText property.
  • Next, we define two buttons for ‘To CSV’ and ‘Clear’. Each button defines @onclick handlers that are bound to their corresponding view model operation (lines #18-27).
  • And, we define a second InputTextArea (lines #30-31) for the converted text. It is set to readonly because this component is used to only display the CSV text (no editing allowed). We use @bind-Value to bind this component to the JsonCsvConverterViewModel.ConvertedText property.
  • Finally, we define an error block (lines #43-48) to show an alert when an ErrorMessage has been set in the view model.

Additional Changes

We need to add another NavLink to the NavMenu for the new JSON-CSV converter page.

@attribute [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">AppIdeas - Converters</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="bin2dec">
                <span class="oi oi-arrow-bottom" aria-hidden="true"></span> Bin2Dec
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="rom2dec">
                <span class="oi oi-move" aria-hidden="true"></span> Roman2Dec
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="number-converter">
                <span class="oi oi-loop-circular" aria-hidden="true"></span> Ultra Number Converter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="dol2cent">
                <span class="oi oi-dollar" aria-hidden="true"></span> Dollar2Cents
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="currency-convert">
                <span class="oi oi-euro" aria-hidden="true"></span> Currency Converter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="json-csv-convert">
                <span class="oi oi-text" aria-hidden="true"></span> JSON-CSV Converter
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

And, we need to register the JsonCsvConverterViewModel class with the dependency injection container in the Program.cs file.

using Blazor.AppIdeas.Converters.Services;
using Blazor.AppIdeas.Converters.ViewModels;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading.Tasks;

namespace Blazor.AppIdeas.Converters
{
    [ExcludeFromCodeCoverage]
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            var httpClient = new HttpClient
            {
                BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
            };
            builder.Services.AddScoped(sp => httpClient);

            // add services and view models to DI container.
            builder.Services.AddSingleton<ICurrencyServiceClient>(
                sp => new CurrencyServiceClient(httpClient));

            builder.Services.AddTransient<RomanDecimalConverter>();
            builder.Services.AddTransient<NumberConverterViewModel>();
            builder.Services.AddTransient<DollarCentsConverterViewModel>();
            builder.Services.AddTransient<CurrencyConverterViewModel>();
            builder.Services.AddTransient<JsonCsvConverterViewModel>();

            await builder.Build().RunAsync();
        }
    }
}

We also have a full suite of test cases to verify the model, view model, and page classes. Please review the commit for this app-idea to review them.

With the code and tests complete, we can build and run the application. Enter some JSON text in the source field and try some different conversions.

Fig 1 – JSON-CSV Converter

In conclusion, we created the basic JSON to CSV conversion features. This portion of the article is similar to the other converters that we have built earlier in this series. In this article, we learned:

  • To use System.Text.Json to parse JSON text into a JsonDocument.
  • To navigate the JsonDocument hierarchy to get at the array, objects, and properties.
  • To use the InputTextArea component to edit and display multi-line text.

3 thoughts on “App-Idea 6: JSON to CSV Converter (Part 1)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s