TopoJSON is a compact encoding of geographic data, and it is common for each feature in a TopoJSON file to carry an attribute object rather than a flat set of key-value pairs. Exporting that structure to CSV is where things usually go wrong: most tools either drop the nested object or dump it back out as a JSON string wedged into a single cell. This guide shows how to convert TopoJSON to CSV in C# using Aspose.GIS, keeping every attribute addressable as its own column and optionally writing the geometry alongside it as Well-Known Text.

The payoff is that a hierarchical GIS dataset becomes a flat table that SQL Server, Power BI, pandas, or a plain spreadsheet can consume directly, with no bespoke JSON parsing layer in your application.

Why Convert TopoJSON to CSV using Aspose.GIS

Consider a TopoJSON feature whose properties look like this:

{
  "name": "North District",
  "prop1": { "this": "that" }
}

A naive CSV writer has three bad options: drop prop1, serialise it as {"this":"that"} into one cell, or fail. All three push the problem downstream, where someone has to re-parse JSON out of a text column before they can filter or join on it.

Aspose.GIS takes a different approach. When the TopoJSON driver reads the file, it projects the nested object into the layer schema as a flat attribute named after its path, joining parent and child with an underscore. The feature above is exposed as two ordinary columns:

ColumnValue
nameNorth District
prop1_thisthat

Because the flattening happens at read time, the conversion step needs no special handling at all. Whatever the layer exposes is what gets written, so prop1_this lands in the CSV as a first-class column you can sort, filter, and join on.

Prerequisites of TopoJSON to CSV Conversion

  • .NET 6.0 or later, or .NET Framework 4.6.2 or later
  • Aspose.GIS for .NET, installed from NuGet
  • A TopoJSON file containing at least one nested attribute object

Install the package with the .NET CLI:

dotnet add package Aspose.GIS

Or from the Package Manager Console:

Install-Package Aspose.GIS

One naming detail catches people out: the NuGet package and assembly are called Aspose.GIS, but the root namespace is Aspose.Gis with a lowercase is. The CSV driver options live in Aspose.Gis.Formats.Csv.

using Aspose.Gis;
using Aspose.Gis.Formats.Csv;

Product details are on the Aspose.GIS for .NET product page, with the full type listing in the API reference.

Inspect the Flattened Schema Before You Convert

What you will accomplish: Open the TopoJSON layer and print its attribute names so you know exactly which columns the CSV will contain.

This step is worth doing once against a representative file. It tells you the real column names produced by flattening, which is far more reliable than guessing at them from the raw JSON.

using System;
using Aspose.Gis;

using (VectorLayer layer = VectorLayer.Open("data/regions.topojson", Drivers.TopoJson))
{
    Console.WriteLine($"Features: {layer.Count}");

    foreach (FeatureAttribute attribute in layer.Attributes)
    {
        Console.WriteLine($"  {attribute.Name} ({attribute.DataType})");
    }
}

For the sample feature shown earlier, this prints name (String) and prop1_this (String). The nested object never appears as an attribute in its own right, which is exactly what you want.


Convert TopoJSON to CSV While Preserving Nested Attributes

What you will accomplish: Load a TopoJSON file, write every flattened attribute to a CSV file, and read the result back to confirm the column made it through.

Step-by-step process

  1. Define the source TopoJSON path and the destination CSV path.
  2. Open the source with VectorLayer.Open and confirm the flattened attribute exists.
  3. Call the static VectorLayer.Convert overload with the source and destination drivers.
  4. Re-open the CSV with Drivers.Csv and check the column count and values.

The following example converts a TopoJSON file to CSV, flattening nested properties along the way.

using System;
using System.IO;
using Aspose.Gis;

class TopoJsonToCsv
{
    static void Main()
    {
        string sourcePath = Path.Combine("data", "regions.topojson");
        string destinationPath = Path.Combine("output", "regions.csv");

        Directory.CreateDirectory("output");

        // Confirm the nested property was flattened when the layer was opened.
        using (VectorLayer source = VectorLayer.Open(sourcePath, Drivers.TopoJson))
        {
            Console.WriteLine($"Source features: {source.Count}");
            Console.WriteLine($"Has prop1_this: {source.Attributes.Contains("prop1_this")}");
        }

        // No options are needed. The conversion writes whatever columns the
        // source layer exposes, flattened attributes included.
        VectorLayer.Convert(sourcePath, Drivers.TopoJson, destinationPath, Drivers.Csv);

        // Read the CSV back and verify the schema survived the round trip.
        using (VectorLayer csv = VectorLayer.Open(destinationPath, Drivers.Csv))
        {
            Console.WriteLine($"Rows: {csv.Count}, Columns: {csv.Attributes.Count}");

            if (csv.Attributes.Contains("prop1_this"))
            {
                Console.WriteLine($"First value: {csv[0].GetValue<string>("prop1_this")}");
            }

            // No WKT column was requested, so features read back without geometry.
            Console.WriteLine($"Geometry empty: {csv[0].Geometry.IsEmpty}");
        }
    }
}

Explanation

  • VectorLayer.Open(string, FileDriver) opens the file read-only. Drivers.TopoJson returns the driver for the TopoJSON format.
  • FeatureAttributeCollection.Contains(string) is the cheapest way to test for a column by name. IndexOf gives you its position if you need it.
  • VectorLayer.Convert(string, FileDriver, string, FileDriver) is a static helper that streams the source into the destination format in one call. There is also an overload taking ConversionOptions, used in the next section.
  • Feature.GetValue<T>(string) reads a typed attribute value. Use GetValueOrDefault<T>(string) when a column may be missing or null, since GetValue<T> assumes the value is present.
  • The final check is the one that surprises people. A CSV file has no geometry column unless you create one, so every feature comes back with an empty geometry. That is the default behaviour, not a conversion failure, and the next section fixes it.

Wrapping both layers in using blocks matters here: VectorLayer implements IDisposable, and the destination file is not guaranteed to be flushed and closed until it is disposed.

Include Geometry as WKT Using CsvOptions

What you will accomplish: Produce a CSV that carries both the flattened attributes and a column holding each feature’s geometry as Well-Known Text.

Step-by-step process

  1. Create a CsvOptions instance and set ColumnWkt to your chosen column name.
  2. Assign it to ConversionOptions.DestinationDriverOptions.
  3. Call the VectorLayer.Convert overload that accepts conversion options.
  4. Re-open the CSV with the same CsvOptions so the driver knows which column to parse as geometry.
using System;
using System.IO;
using Aspose.Gis;
using Aspose.Gis.Formats.Csv;

class TopoJsonToCsvWithGeometry
{
    static void Main()
    {
        string sourcePath = Path.Combine("data", "regions.topojson");
        string destinationPath = Path.Combine("output", "regions-wkt.csv");

        Directory.CreateDirectory("output");

        // Ask the CSV driver for a geometry column named "GeometryWkt".
        var csvOptions = new CsvOptions
        {
            ColumnWkt = "GeometryWkt"
        };

        var conversionOptions = new ConversionOptions
        {
            DestinationDriverOptions = csvOptions
        };

        VectorLayer.Convert(
            sourcePath,
            Drivers.TopoJson,
            destinationPath,
            Drivers.Csv,
            conversionOptions);

        // Pass the same options when reading, or the WKT column is just text.
        using (VectorLayer layer = VectorLayer.Open(destinationPath, Drivers.Csv, csvOptions))
        {
            foreach (Feature feature in layer)
            {
                string name = feature.GetValueOrDefault<string>("name");
                string wkt = feature.Geometry.IsEmpty ? "(empty)" : feature.Geometry.AsText();

                Console.WriteLine($"{name}: {wkt}");
            }
        }
    }
}

Explanation

  • CsvOptions.ColumnWkt names the column that holds each feature’s geometry in WKT form. It defaults to null, and leaving it null or empty is precisely why the earlier example produced no geometry.
  • ConversionOptions.DestinationDriverOptions is typed as DriverOptions, the base class for every driver’s settings object. Passing a CsvOptions where the destination driver is Drivers.Csv is what makes the setting take effect; passing options of the wrong type for a driver raises an ArgumentException.
  • The same CsvOptions instance is reused on the read side. Without it, the CSV driver has no reason to treat GeometryWkt as anything other than another string column.
  • IGeometry.AsText() returns the ISO WKT representation, so the printed output matches what was written to the file.

The symmetry is the point: the attribute columns behave identically whether or not geometry is included, so you can turn the WKT column on and off without touching the rest of your pipeline.

Reproject During Conversion

If the consuming system expects a specific coordinate system, ask for it as part of the conversion rather than post-processing the CSV. ConversionOptions carries a spatial reference alongside the driver options:

var conversionOptions = new ConversionOptions
{
    DestinationDriverOptions = new CsvOptions { ColumnWkt = "GeometryWkt" },
    DestinationSpatialReferenceSystem = SpatialReferenceSystem.Wgs84
};

VectorLayer.Convert(sourcePath, Drivers.TopoJson, destinationPath, Drivers.Csv, conversionOptions);

Not every destination driver supports every spatial reference system. Call Drivers.Csv.SupportsSpatialReferenceSystem(...) first if the target system is configurable at runtime, and fall back to a null options object when it is not supported.

Batch Convert a Folder of TopoJSON Files

The API is identical for one file and for a thousand. The only rule worth remembering when you parallelise is that each worker needs its own layer and options objects.

using System;
using System.IO;
using System.Threading.Tasks;
using Aspose.Gis;
using Aspose.Gis.Formats.Csv;

static void ConvertFolder(string inputFolder, string outputFolder)
{
    Directory.CreateDirectory(outputFolder);

    var files = Directory.GetFiles(inputFolder, "*.topojson");

    Parallel.ForEach(files, file =>
    {
        string destination = Path.Combine(
            outputFolder,
            Path.GetFileNameWithoutExtension(file) + ".csv");

        // Fresh options per iteration: nothing is shared across threads.
        var options = new ConversionOptions
        {
            DestinationDriverOptions = new CsvOptions { ColumnWkt = "GeometryWkt" }
        };

        try
        {
            VectorLayer.Convert(file, Drivers.TopoJson, destination, Drivers.Csv, options);
        }
        catch (GisException ex)
        {
            Console.Error.WriteLine($"Skipped {Path.GetFileName(file)}: {ex.Message}");
        }
    });
}

Catching GisException per file rather than around the whole loop means one malformed TopoJSON file logs a warning instead of aborting the batch.

Get a Free License

Aspose.GIS runs in evaluation mode until a license is applied. Request a free temporary license from the Aspose temporary license page and apply it once at application startup, before any other Aspose.GIS call:

var license = new Aspose.Gis.License();
license.SetLicense("Aspose.GIS.NET.lic");

Free Additional Resources

Conclusion

Converting TopoJSON with nested properties to CSV in .NET comes down to one useful piece of behaviour: Aspose.GIS flattens nested attribute objects into named columns such as prop1_this while the layer is being read, so VectorLayer.Convert needs no custom logic to preserve them. Add CsvOptions.ColumnWkt when the downstream system needs geometry, add DestinationSpatialReferenceSystem when it needs a specific projection, and loop the same call when you need to process a folder. The result is a flat, analysis-ready table produced by a handful of lines of C#, with no JSON parsing layer of your own to maintain.

FAQs

  1. Can Aspose.GIS flatten nested JSON objects when converting to CSV? Yes. The TopoJSON driver exposes a nested attribute object as separate columns when the layer is opened, joining parent and child names with an underscore, so the flattening is already done before the conversion runs.

  2. Do I need to write custom code to handle nested properties? No. VectorLayer.Convert writes whatever columns the source layer exposes, so nested properties need no extra parsing code of your own.

  3. How can I include geometry in the CSV output? Create a CsvOptions instance, set its ColumnWkt property to the column name you want, assign it to ConversionOptions.DestinationDriverOptions, and pass that to VectorLayer.Convert.

  4. Why is the geometry empty when I read my CSV back? A CSV has no geometry column unless you ask for one. Set ColumnWkt on the CsvOptions you pass both when writing and when reading, otherwise the driver has no column from which to build a geometry.

  5. What exceptions should I handle during conversion? The documented ones are ArgumentNullException for a null path, ArgumentException when an options object does not match the driver, GisException when a feature cannot be read from the file, and IOException for underlying I/O failures.

  6. Can I convert a batch of TopoJSON files at once? Yes. Loop over the file list and call VectorLayer.Convert for each file. If you parallelise the loop, give every worker its own VectorLayer and options instances rather than sharing one across threads.

  7. Does the conversion change the coordinate system? Not by default. To reproject during conversion, set ConversionOptions.DestinationSpatialReferenceSystem to the target system before calling Convert.

Read More