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 aJsonElement
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 callsParseJsonElement
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 firstJsonElement
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 theSourceText
and produces aJsonDocument
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 theJsonValueKind
of the root element is anything other thanObject
orArray
, then we throw aNotSupportedException
.
- The
- 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 theSourceText
. - 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.
- If we don’t have a value in
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 aTextConverter
with theSourceText
and callsJsonToCsv
. Storing the returned value inConvertedText
. 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 theJsonCsvConverterViewModel.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 toreadonly
because this component is used to only display the CSV text (no editing allowed). We use@bind-Value
to bind this component to theJsonCsvConverterViewModel.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.

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 aJsonDocument
. - 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)”