Markout
Markup adds instructions to content. Markout removes structure from data.
Markout is a source-generated .NET serializer that projects objects into structured, human-readable documents. You annotate your models with attributes that describe data relationships — identity, enumeration, tabulation, measurement, hierarchy — and the source generator emits code that writes through an abstract renderer. The same object graph produces Markdown tables, ANSI terminal output with colored bars, plain text with aligned columns, pretty tables, or TSV rows, without the developer making visual decisions.
Quick Start
using Markout;
var artist = new Artist(
Name: "Sarah McLachlan",
Genre: "Pop / Adult Contemporary",
Origin: "Halifax, Nova Scotia",
DebutYear: 1988,
BestKnownFor: "Angel, Building a Mystery, Adia");
MarkoutSerializer.Serialize(artist, Console.Out, ArtistContext.Default);
[MarkoutSerializable(TitleProperty = nameof(Artist.Name))]
public record Artist(
string Name,
string Genre,
string Origin,
int DebutYear,
string BestKnownFor);
[MarkoutContext(typeof(Artist))]
public partial class ArtistContext : MarkoutSerializerContext { }
Output:
# Sarah McLachlan
| Field | Value |
| ----- | ----- |
| Genre | Pop / Adult Contemporary |
| Origin | Halifax, Nova Scotia |
| Debut Year | 1988 |
| Best Known For | Angel, Building a Mystery, Adia |
Three things: a record, a context, one line of serialization. The TitleProperty becomes a heading; everything else renders as a field table.
Field Layouts
The same model renders differently with FieldLayout. The default is a two-column table. Switch to Inline for a compact summary line:
[MarkoutSerializable(TitleProperty = nameof(Artist.Name), FieldLayout = FieldLayout.Inline)]
public record Artist( ... );
# Sarah McLachlan
Genre: Pop / Adult Contemporary | Origin: Halifax, Nova Scotia | Debut Year: 1988 | Best Known For: Angel, Building a Mystery, Adia
Or Bulleted for a list:
[MarkoutSerializable(TitleProperty = nameof(Artist.Name), FieldLayout = FieldLayout.Bulleted)]
public record Artist( ... );
# Sarah McLachlan
- Genre: Pop / Adult Contemporary
- Origin: Halifax, Nova Scotia
- Debut Year: 1988
- Best Known For: Angel, Building a Mystery, Adia
Or Numbered:
# Sarah McLachlan
1. Genre: Pop / Adult Contemporary
2. Origin: Halifax, Nova Scotia
3. Debut Year: 1988
4. Best Known For: Angel, Building a Mystery, Adia
Or Plain — bare lines, no markers. Each line ends with two trailing spaces, which is the markdown signal for <br>:
# Sarah McLachlan
Genre: Pop / Adult Contemporary
Origin: Halifax, Nova Scotia
Debut Year: 1988
Best Known For: Angel, Building a Mystery, Adia
The data model doesn't change — only the attribute controls the shape.
Adding Sections and Tables
Add [MarkoutSection] to group properties with headings, and use List<T> for tables:
[MarkoutSerializable(TitleProperty = nameof(Title))]
public class CityReport
{
public string Title { get; set; } = "";
public string Province { get; set; } = "";
public int Population { get; set; }
[MarkoutSection(Name = "Landmarks")]
public List<LandmarkRow>? Landmarks { get; set; }
}
[MarkoutSerializable]
public class LandmarkRow
{
public string Name { get; set; } = "";
public string Type { get; set; } = "";
public int Year { get; set; }
}
[MarkoutContext(typeof(CityReport))]
[MarkoutContext(typeof(LandmarkRow))]
public partial class ReportContext : MarkoutSerializerContext { }
MarkoutSerializer.Serialize(city, Console.Out, ReportContext.Default);
Output:
# Vancouver
| Field | Value |
| ----- | ----- |
| Province | British Columbia |
| Population | 2632000 |
## Landmarks
| Name | Type | Year |
| ----- | ----- | ----- |
| Stanley Park | Park | 1888 |
| Gastown | Historic | 1867 |
| Science World | Museum | 1989 |
Real-World Example: GitHub Repository Report
The GitHubRepo sample fetches four GitHub API endpoints in parallel and projects the combined JSON into a single report — fields, bar charts, contributor metrics, and release tables — all from one model:
[MarkoutSerializable(TitleProperty = nameof(Title), DescriptionProperty = nameof(Description))]
public class RepoInfo
{
public string Title { get; set; } = "";
[MarkoutIgnore]
public string Description { get; set; } = "";
[MarkoutDisplayFormat("{0:N0}")]
public int Stars { get; set; }
[MarkoutDisplayFormat("{0:N0}")]
public int Forks { get; set; }
[MarkoutDisplayFormat("{0:N0}")]
public int OpenIssues { get; set; }
public string Language { get; set; } = "";
public string License { get; set; } = "";