JSON Serialization
There are three ways to move JSON in and out of a Transpose application, in increasing order of capability and cost.
1. The native JSON object
Transpose.Core binds the browser's JSON global as es5.JSON:
using System;
using Transpose.Core;
public static class Json
{
public static string Serialize(object obj)
{
return es5.JSON.stringify(obj);
}
public static object Deserialize(string json)
{
return es5.JSON.parse(json);
}
}
A using static Transpose.Core.es5; lets you write JSON.stringify(obj) directly.
This is the cheapest option and it is exactly what the browser does, which is also
its limitation: it serializes the JavaScript shape of a value. A plain C# class
carries runtime type machinery that ends up in the output, and a List<T>,
Dictionary<K,V>, DateTime, Guid, long or decimal is a runtime object
rather than a plain value — see
Working with Data. Use it for values that
are already plain (strings, numbers, arrays of those) or in combination with
[ObjectLiteral] below.
2. [ObjectLiteral] types
[ObjectLiteral] tells the compiler to emit a class or struct as a plain
JavaScript object — no prototype chain, no type metadata:
using Transpose;
[ObjectLiteral]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
var p = new Person { Name = "Alice", Age = 30 };
// emits: var p = { Name: "Alice", Age: 30 };
es5.JSON.stringify(p) then produces {"Name":"Alice","Age":30} with no
adaptation, and a value that came out of es5.JSON.parse can be used as a
Person directly.
This is the right shape for data transfer objects: the payloads you exchange
with a server, and anything you hand to a JavaScript library that expects an
options object. Keep them to plain members — strings, numbers, booleans, arrays,
and other [ObjectLiteral] types — and represent long, decimal, DateTime
and Guid as string so they round-trip.
Because an [ObjectLiteral] instance has no runtime type, it cannot take part in
virtual dispatch, is/as checks against a base type, or reflection. Use it for
data, not for behaviour.
3. Transpose.Newtonsoft.Json
For real serialization of ordinary C# classes — attributes, converters,
polymorphism — use the
Transpose.Newtonsoft.Json
package, which implements the Json.NET API surface in the browser.
Installation
dotnet add package Transpose.Newtonsoft.Json
Usage
The API mirrors Json.NET:
using Newtonsoft.Json;
public class User
{
public string Name { get; set; }
[JsonProperty("user_age")]
public int Age { get; set; }
}
var user = new User { Name = "Bob", Age = 25 };
string json = JsonConvert.SerializeObject(user);
// {"Name":"Bob","user_age":25}
var user2 = JsonConvert.DeserializeObject<User>(json);
Supported:
- Custom property names (
[JsonProperty]) and exclusions ([JsonIgnore]). [JsonConstructor],[DefaultValue], and the serialization callbacks.JsonSerializerSettings— null and default-value handling, the camel-case contract resolver, object-creation handling.- BCL types:
DateTime,TimeSpan,Guid,Uri,decimal, 64-bit integers, byte arrays, nullables. - Collections, dictionaries, sets and the collection interfaces.
- Polymorphic payloads (
TypeNameHandling,$type,ISerializationBinder). JsonConvert.PopulateObject.- The JSON superset Json.NET accepts — comments, single quotes, unquoted names, trailing commas — through the package's own reader.
Known divergences from Json.NET
The package is tested by running each snippet twice, once against the real Json.NET on .NET and once as translated JavaScript on Node, and diffing the output. The differences below are the ones that remain, and each is pinned by a test:
| Behaviour | Json.NET | Transpose.Newtonsoft.Json |
|---|---|---|
| Member order | Declaration order (fields first) | Alphabetical (fields first) |
long / ulong |
JSON numbers | JSON strings — JavaScript cannot hold them exactly |
Whole-number double |
1.0 |
1 |
| A reference cycle | Throws | Drops the back-reference |
null into a non-nullable value member |
Throws | Leaves the default |
| Empty input for a value type | Throws | Returns the default |
| An array where an object is expected (and vice versa) | Throws | Empty instance / empty collection |
| A private setter | Not written unless [JsonProperty] |
Always written |
Deserializing to object |
A JObject |
The raw parsed JavaScript value |
Nullable<TEnum> |
Prints the member name | Prints the number (comparisons are unaffected) |
| An unknown enum name | JsonSerializationException |
ArgumentException |
The long-as-string rule is the one most likely to affect a client and server that
share DTOs: a 64-bit id serialized by this package arrives as a JSON string. Either
type such fields as string on both sides, or configure the server to accept a
string there.
Linq-to-JSON (JObject, JArray, JToken navigation) is not the package's focus —
deserializing to object gives you the parsed JavaScript value instead. Prefer a
typed DTO.
No eval
Strict JSON goes through the browser's JSON.parse. A payload using Json.NET's
lenient syntax falls back to the package's own hand-written reader, which does not
use eval or new Function — so the package works under a Content-Security-Policy
without 'unsafe-eval'. That reader is also slightly stricter than Json.NET's
evaluator was: it rejects undefined, a leading + on a number, and a signed
hexadecimal literal.
Which to use
Native JSON |
[ObjectLiteral] |
Transpose.Newtonsoft.Json |
|
|---|---|---|---|
| Bundle cost | none | none | a package |
| Handles ordinary C# classes | no | no | yes |
| Attributes, converters, polymorphism | no | no | yes |
| Interops with JS as a plain value | yes | yes | no |
| Best for | already-plain values | DTOs and JS options objects | full serialization |
For a typical application: [ObjectLiteral] DTOs for the wire format, and
Transpose.Newtonsoft.Json when the payload has to carry type information or the
DTOs are shared with server code you cannot reshape.