QsNet 1.0.11

dotnet add package QsNet --version 1.0.11
                    
NuGet\Install-Package QsNet -Version 1.0.11
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="QsNet" Version="1.0.11" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="QsNet" Version="1.0.11" />
                    
Directory.Packages.props
<PackageReference Include="QsNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add QsNet --version 1.0.11
                    
#r "nuget: QsNet, 1.0.11"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package QsNet@1.0.11
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=QsNet&version=1.0.11
                    
Install as a Cake Addin
#tool nuget:?package=QsNet&version=1.0.11
                    
Install as a Cake Tool

QsNet

QsNet

A query string encoding and decoding library for C#/.NET.

Ported from qs for JavaScript.

Targets DocFX Docs NuGet Version NuGet Downloads Test codecov Codacy Badge GitHub GitHub Repo stars


Highlights

  • Nested dictionaries and lists: foo[bar][baz]=qux{ "foo": { "bar": { "baz": "qux" } } }
  • Multiple list formats (indices, brackets, repeat, comma)
  • Dot-notation support (a.b=c) and "."-encoding toggles
  • UTF-8 and Latin1 charsets, plus optional charset sentinel (utf8=✓)
  • Custom encoders/decoders, key sorting, filtering, and strict null handling
  • Supports DateTime serialization via a pluggable serializer
  • Extensive tests (xUnit + FluentAssertions), performance-minded implementation

Installation

NuGet Package Manager

Install-Package QsNet

.NET CLI

dotnet add package QsNet

Package Reference


<PackageReference Include="QsNet" Version="<version>"/>

Requirements

  • Target frameworks (TFMs): net8.0, netstandard2.0
  • Supported runtimes (via the target frameworks above):
Runtime Version CI Coverage Status
.NET 10 (preview) Full CI (experimental; non-blocking) Preview
.NET 9 (STS) Full CI Supported
.NET 8 (LTS) Full CI Supported
.NET 7 Consumer smoke test Supported
.NET 6 (LTS) Consumer smoke test Supported
.NET 5 Optional smoke (non-blocking) EOL
.NET Core 3.1 Compile-only smoke EOL
.NET Framework 4.6.1+ Smoke test (4.6.1, 4.8.1) Supported
  • Platforms: Windows, Linux, macOS (cross-platform, no native dependencies)

Quick start

using QsNet;

// Decode
Dictionary<string, object?> obj = Qs.Decode("foo[bar]=baz&foo[list][]=a&foo[list][]=b");
// -> { "foo": { "bar": "baz", "list": ["a", "b"] } }

// Encode
string qs = Qs.Encode(new Dictionary<string, object?> 
{ 
    ["foo"] = new Dictionary<string, object?> { ["bar"] = "baz" } 
});
// -> "foo%5Bbar%5D=baz"

Usage

Simple

// Decode
Dictionary<string, object?> decoded = Qs.Decode("a=c");
// => { "a": "c" }

// Encode
string encoded = Qs.Encode(new Dictionary<string, object?> { ["a"] = "c" });
// => "a=c"

Decoding

Nested dictionaries

Qs.Decode("foo[bar]=baz");
// => { "foo": { "bar": "baz" } }

Qs.Decode("a%5Bb%5D=c");
// => { "a": { "b": "c" } }

Qs.Decode("foo[bar][baz]=foobarbaz");
// => { "foo": { "bar": { "baz": "foobarbaz" } } }

Depth (default: 5)

Beyond the configured depth, remaining bracket content is kept as literal text:

Qs.Decode("a[b][c][d][e][f][g][h][i]=j");
// => { "a": { "b": { "c": { "d": { "e": { "f": { "[g][h][i]": "j" } } } } } } }

Override depth:

Qs.Decode("a[b][c][d][e][f][g][h][i]=j", new DecodeOptions { Depth = 1 });
// => { "a": { "b": { "[c][d][e][f][g][h][i]": "j" } } }

Parameter limit

Qs.Decode("a=b&c=d", new DecodeOptions { ParameterLimit = 1 });
// => { "a": "b" }

Ignore leading ?

Qs.Decode("?a=b&c=d", new DecodeOptions { IgnoreQueryPrefix = true });
// => { "a": "b", "c": "d" }

Custom delimiter (string or regex)

Qs.Decode("a=b;c=d", new DecodeOptions { Delimiter = new StringDelimiter(";") });
// => { "a": "b", "c": "d" }

Qs.Decode("a=b;c=d", new DecodeOptions { Delimiter = new RegexDelimiter("[;,]") });
// => { "a": "b", "c": "d" }

Dot-notation and "decode dots in keys"

Qs.Decode("a.b=c", new DecodeOptions { AllowDots = true });
// => { "a": { "b": "c" } }

Qs.Decode(
    "name%252Eobj.first=John&name%252Eobj.last=Doe",
    new DecodeOptions { DecodeDotInKeys = true }
);
// => { "name.obj": { "first": "John", "last": "Doe" } }

Empty lists

Qs.Decode("foo[]&bar=baz", new DecodeOptions { AllowEmptyLists = true });
// => { "foo": [], "bar": "baz" }

Duplicates

Qs.Decode("foo=bar&foo=baz");
// => { "foo": ["bar", "baz"] }

Qs.Decode("foo=bar&foo=baz", new DecodeOptions { Duplicates = Duplicates.Combine });
// => same as above

Qs.Decode("foo=bar&foo=baz", new DecodeOptions { Duplicates = Duplicates.First });
// => { "foo": "bar" }

Qs.Decode("foo=bar&foo=baz", new DecodeOptions { Duplicates = Duplicates.Last });
// => { "foo": "baz" }

Charset and sentinel

// Latin1
Qs.Decode("a=%A7", new DecodeOptions { Charset = Encoding.Latin1 });
// => { "a": "§" }

// Sentinels
Qs.Decode("utf8=%E2%9C%93&a=%C3%B8", new DecodeOptions { Charset = Encoding.Latin1, CharsetSentinel = true });
// => { "a": "ø" }

Qs.Decode("utf8=%26%2310003%3B&a=%F8", new DecodeOptions { Charset = Encoding.UTF8, CharsetSentinel = true });
// => { "a": "ø" }

Interpret numeric entities (&#1234;)

Qs.Decode(
    "a=%26%239786%3B",
    new DecodeOptions { Charset = Encoding.Latin1, InterpretNumericEntities = true }
);
// => { "a": "☺" }

Lists

Qs.Decode("a[]=b&a[]=c");
// => { "a": ["b", "c"] }

Qs.Decode("a[1]=c&a[0]=b");
// => { "a": ["b", "c"] }

Qs.Decode("a[1]=b&a[15]=c");
// => { "a": ["b", "c"] }

Qs.Decode("a[]=&a[]=b");
// => { "a": ["", "b"] }

Large indices convert to a dictionary by default:

Qs.Decode("a[100]=b");
// => { "a": { 100: "b" } }

Disable list parsing:

Qs.Decode("a[]=b", new DecodeOptions { ParseLists = false });
// => { "a": { 0: "b" } }

Mixing notations merges into a dictionary:

Qs.Decode("a[0]=b&a[b]=c");
// => { "a": { 0: "b", "b": "c" } }

Comma-separated values:

Qs.Decode("a=b,c", new DecodeOptions { Comma = true });
// => { "a": ["b", "c"] }

Primitive/scalar values

All values decode as strings by default:

Qs.Decode("a=15&b=true&c=null");
// => { "a": "15", "b": "true", "c": "null" }

Encoding

Basics

Qs.Encode(new Dictionary<string, object?> { ["a"] = "b" });
// => "a=b"

Qs.Encode(new Dictionary<string, object?> 
{ 
    ["a"] = new Dictionary<string, object?> { ["b"] = "c" } 
});
// => "a%5Bb%5D=c"

Disable URI encoding for readability:

Qs.Encode(
    new Dictionary<string, object?> 
    { 
        ["a"] = new Dictionary<string, object?> { ["b"] = "c" } 
    },
    new EncodeOptions { Encode = false }
);
// => "a[b]=c"

Values-only encoding:

Qs.Encode(
    new Dictionary<string, object?>
    {
        ["a"] = "b",
        ["c"] = new List<object?> { "d", "e=f" },
        ["f"] = new List<object?>
        {
            new List<object?> { "g" },
            new List<object?> { "h" },
        },
    },
    new EncodeOptions { EncodeValuesOnly = true }
);
// => "a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h"

Custom encoder:

Qs.Encode(
    new Dictionary<string, object?>
    {
        ["a"] = new Dictionary<string, object?> { ["b"] = "č" },
    },
    new EncodeOptions
    {
        Encoder = (str, _, _) => str?.ToString() == "č" ? "c" : str?.ToString() ?? "",
    }
);
// => "a[b]=c"

List formats

var data = new Dictionary<string, object?> { ["a"] = new List<object?> { "b", "c" } };
var options = new EncodeOptions { Encode = false };

// default (indices)
Qs.Encode(data, options.CopyWith(listFormat: ListFormat.Indices));
// => "a[0]=b&a[1]=c"

// brackets
Qs.Encode(data, options.CopyWith(listFormat: ListFormat.Brackets));
// => "a[]=b&a[]=c"

// repeat
Qs.Encode(data, options.CopyWith(listFormat: ListFormat.Repeat));
// => "a=b&a=c"

// comma
Qs.Encode(data, options.CopyWith(listFormat: ListFormat.Comma));
// => "a=b,c"

Nested dictionaries

Qs.Encode(
    new Dictionary<string, object?>
    {
        ["a"] = new Dictionary<string, object?>
        {
            ["b"] = new Dictionary<string, object?> { ["c"] = "d", ["e"] = "f" },
        },
    },
    new EncodeOptions { Encode = false }
);
// => "a[b][c]=d&a[b][e]=f"

Dot notation:

Qs.Encode(
    new Dictionary<string, object?>
    {
        ["a"] = new Dictionary<string, object?>
        {
            ["b"] = new Dictionary<string, object?> { ["c"] = "d", ["e"] = "f" },
        },
    },
    new EncodeOptions { Encode = false, AllowDots = true }
);
// => "a.b.c=d&a.b.e=f"

Encode dots in keys:

Qs.Encode(
    new Dictionary<string, object?>
    {
        ["name.obj"] = new Dictionary<string, object?>
        {
            ["first"] = "John",
            ["last"] = "Doe",
        },
    },
    new EncodeOptions { AllowDots = true, EncodeDotInKeys = true }
);
// => "name%252Eobj.first=John&name%252Eobj.last=Doe"

Allow empty lists:

Qs.Encode(
    new Dictionary<string, object?> { ["foo"] = new List<object?>(), ["bar"] = "baz" },
    new EncodeOptions { Encode = false, AllowEmptyLists = true }
);
// => "foo[]&bar=baz"

Empty strings and nulls:

Qs.Encode(new Dictionary<string, object?> { ["a"] = "" });
// => "a="

Return empty string for empty containers:

Qs.Encode(new Dictionary<string, object?> { ["a"] = new List<object?>() });        // => ""
Qs.Encode(new Dictionary<string, object?> { ["a"] = new Dictionary<string, object?>() });    // => ""
Qs.Encode(new Dictionary<string, object?> { ["a"] = new List<object?> { new Dictionary<string, object?>() } }); // => ""
Qs.Encode(new Dictionary<string, object?> { ["a"] = new Dictionary<string, object?> { ["b"] = new List<object?>() } }); // => ""
Qs.Encode(new Dictionary<string, object?> { ["a"] = new Dictionary<string, object?> { ["b"] = new Dictionary<string, object?>() } }); // => ""

Omit Undefined:

Qs.Encode(new Dictionary<string, object?> { ["a"] = null, ["b"] = Undefined.Create() });
// => "a="

Add query prefix:

Qs.Encode(
    new Dictionary<string, object?> { ["a"] = "b", ["c"] = "d" },
    new EncodeOptions { AddQueryPrefix = true }
);
// => "?a=b&c=d"

Custom delimiter:

Qs.Encode(
    new Dictionary<string, object?> { ["a"] = "b", ["c"] = "d" },
    new EncodeOptions { Delimiter = ";" }
);
// => "a=b;c=d"

Dates

By default, DateTime is serialized using ToString() in ISO 8601 format.

var date = new DateTime(1970, 1, 1, 0, 0, 0, 7, DateTimeKind.Utc);

Qs.Encode(
    new Dictionary<string, object?> { ["a"] = date },
    new EncodeOptions { Encode = false }
);
// => "a=1970-01-01T00:00:00.0070000Z"

Qs.Encode(
    new Dictionary<string, object?> { ["a"] = date },
    new EncodeOptions
    {
        Encode = false,
        DateSerializer = d => ((DateTimeOffset)d).ToUnixTimeMilliseconds().ToString(),
    }
);
// => "a=7"

Sorting & filtering

// Sort keys
Qs.Encode(
    new Dictionary<string, object?>
    {
        ["a"] = "c",
        ["z"] = "y",
        ["b"] = "f",
    },
    new EncodeOptions
    {
        Encode = false,
        Sort = (a, b) => string.Compare(a?.ToString(), b?.ToString(), StringComparison.Ordinal),
    }
);
// => "a=c&b=f&z=y"

// Filter by function (drop/transform values)
var epochStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var testDate = epochStart.AddMilliseconds(123);

Qs.Encode(
    new Dictionary<string, object?>
    {
        ["a"] = "b",
        ["c"] = "d",
        ["e"] = new Dictionary<string, object?>
        {
            ["f"] = testDate,
            ["g"] = new List<object?> { 2 },
        },
    },
    new EncodeOptions
    {
        Encode = false,
        Filter = new FunctionFilter(
            (prefix, value) =>
                prefix switch
                {
                    "b" => Undefined.Create(),
                    "e[f]" => (long)((DateTime)value! - epochStart).TotalMilliseconds,
                    "e[g][0]" => Convert.ToInt32(value) * 2,
                    _ => value,
                }
        ),
    }
);
// => "a=b&c=d&e[f]=123&e[g][0]=4"

// Filter by explicit list of keys/indices
Qs.Encode(
    new Dictionary<string, object?>
    {
        ["a"] = "b",
        ["c"] = "d",
        ["e"] = "f",
    },
    new EncodeOptions
    {
        Encode = false,
        Filter = new IterableFilter(new List<object> { "a", "e" }),
    }
);
// => "a=b&e=f"

Qs.Encode(
    new Dictionary<string, object?>
    {
        ["a"] = new List<object?> { "b", "c", "d" },
        ["e"] = "f",
    },
    new EncodeOptions
    {
        Encode = false,
        Filter = new IterableFilter(new List<object> { "a", 0, 2 }),
    }
);
// => "a[0]=b&a[2]=d"

Null handling

// Treat null values like empty strings by default
Qs.Encode(new Dictionary<string, object?> { ["a"] = null, ["b"] = "" });
// => "a=&b="

// Cannot distinguish between parameters with and without equal signs
Qs.Decode("a&b=");
// => { "a": "", "b": "" }

// Distinguish between null values and empty strings using strict null handling
Qs.Encode(
    new Dictionary<string, object?> { ["a"] = null, ["b"] = "" },
    new EncodeOptions { StrictNullHandling = true }
);
// => "a&b="

// Decode values without equals back to null using strict null handling
Qs.Decode("a&b=", new DecodeOptions { StrictNullHandling = true });
// => { "a": null, "b": "" }

// Completely skip rendering keys with null values using skip nulls
Qs.Encode(
    new Dictionary<string, object?> { ["a"] = "b", ["c"] = null },
    new EncodeOptions { SkipNulls = true }
);
// => "a=b"

Charset handling

Note (Latin-1 on older TFMs): Some frameworks (e.g., netstandard2.0) don’t expose Encoding.Latin1 directly. Use Encoding.GetEncoding("iso-8859-1"). On .NET Core / netstandard you may also need to register the code pages provider:

using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var latin1 = Encoding.GetEncoding("iso-8859-1");
// Encode using Latin1 charset
Qs.Encode(
    new Dictionary<string, object?> { ["æ"] = "æ" },
    new EncodeOptions { Charset = Encoding.Latin1 }
);
// => "%E6=%E6"

// Convert characters that don't exist in Latin1 to numeric entities
Qs.Encode(
    new Dictionary<string, object?> { ["a"] = "☺" },
    new EncodeOptions { Charset = Encoding.Latin1 }
);
// => "a=%26%239786%3B"

// Announce charset using charset sentinel option with UTF-8
Qs.Encode(
    new Dictionary<string, object?> { ["a"] = "☺" },
    new EncodeOptions { CharsetSentinel = true }
);
// => "utf8=%E2%9C%93&a=%E2%98%BA"

// Announce charset using charset sentinel option with Latin1
Qs.Encode(
    new Dictionary<string, object?> { ["a"] = "æ" },
    new EncodeOptions { Charset = Encoding.Latin1, CharsetSentinel = true }
);
// => "utf8=%26%2310003%3B&a=%E6"

RFC 3986 vs RFC 1738 space encoding

Qs.Encode(new Dictionary<string, object?> { ["a"] = "b c" });
// => "a=b%20c"   (RFC 3986 default)

Qs.Encode(new Dictionary<string, object?> { ["a"] = "b c" }, new EncodeOptions { Format = Format.Rfc3986 });
// => "a=b%20c"

Qs.Encode(new Dictionary<string, object?> { ["a"] = "b c" }, new EncodeOptions { Format = Format.Rfc1738 });
// => "a=b+c"

Design notes

  • Performance: The implementation mirrors qs semantics but is optimized for C#/.NET. Deep parsing, list compaction, and cycle-safe compaction are implemented iteratively where it matters.
  • Safety: Defaults (depth, parameterLimit) help mitigate abuse in user-supplied inputs; you can loosen them when you fully trust the source.
  • Interop: Exposes knobs similar to qs (filters, sorters, custom encoders/decoders) to make migrations straightforward.

Other ports

Port Repository Package
Dart techouse/qs pub.dev
Python techouse/qs_codec PyPI
Kotlin / JVM + Android AAR techouse/qs-kotlin Maven Central
Swift / Objective-C techouse/qs-swift SPM
Node.js (original) ljharb/qs npm

Special thanks to the authors of qs for JavaScript:


License

BSD 3-Clause © techouse

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.11 36 9/2/2025
1.0.10 173 8/30/2025
1.0.9 205 8/27/2025
1.0.8 182 8/26/2025
1.0.7 89 8/24/2025
1.0.6 60 8/23/2025
1.0.5 77 8/22/2025
1.0.4 138 8/17/2025
1.0.3 109 8/17/2025
1.0.2 149 8/13/2025
1.0.1 182 8/8/2025
1.0.0 221 8/7/2025