<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Protojsonx on kmcd.dev</title><link>https://kmcd.dev/tags/protojsonx/</link><description>Recent content in Protojsonx on kmcd.dev</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>All Rights Reserved</copyright><lastBuildDate>Tue, 21 Jul 2026 15:20:00 +0000</lastBuildDate><atom:link href="https://kmcd.dev/tags/protojsonx/index.xml" rel="self" type="application/rss+xml"/><item><title>Beating Go's encoding/json with Schema-Guided ProtoJSON</title><link>https://kmcd.dev/posts/benchmarking-protojsonx/</link><pubDate>Tue, 21 Jul 2026 15:20:00 +0000</pubDate><guid>https://kmcd.dev/posts/benchmarking-protojsonx/</guid><description> 
                
                How protojsonx compares to standard protojson, JSON, and vtproto.
                </description><content:encoded><![CDATA[<p>In my <a href="https://kmcd.dev/posts/hidden-cost-of-google-protobuf-value/">previous article</a>, I explored the performance bottlenecks of using dynamic JSON structures like <code>google.protobuf.Value</code> and official <code>protojson</code> in Go. The benchmark results pointed at the same culprit over and over: reflection, pointer chasing, and descriptor traversal were showing up as real latency and allocation costs.</p>
<p>ProtoJSON sits in an awkward place. It gives Protobuf-backed APIs a JSON representation that human operators, browsers, API gateways, and debugging tools can work with, but Go’s official <code>protojson</code> implementation pays heavily for this generality. To resolve field names, enum values, presence semantics, and message structure at runtime, it has to walk message descriptors using Protobuf reflection (<code>protoreflect</code>) and allocate intermediate values.</p>
<p>This is the right tradeoff for correctness and compatibility. But it made me wonder: <em>how much of this cost is fundamental to ProtoJSON, and how much comes from repeatedly resolving schema mappings at runtime?</em></p>
<p>So I built <a href="https://github.com/sudorandom/protojsonx" rel="external">protojsonx</a>, partly as an experiment and partly because I wanted to know whether ProtoJSON was inherently slow or whether Go’s implementation was paying too much runtime bookkeeping cost. It implements two strategies: compiling descriptors into flat offset layout tables once at startup (<strong>Runtime Table Mode</strong>), and generating static, reflection-free parsing routines via a protoc plugin (<strong>Generated Plugin Mode</strong>).</p>
<p>In this post, we&rsquo;ll look at the benchmarks comparing standard <code>protojson</code>, raw struct-based JSON serializers (<code>encoding/json</code> and <code>encoding/json/v2</code> currently living at <code>github.com/go-json-experiment/json</code>), <code>protojsonx</code> in both modes, and raw binary Protobuf (<code>proto</code> and the optimized reflection-free <code>vtproto</code>). The interesting part is that moving schema work out of the hot path gets ProtoJSON much closer to ordinary JSON and binary protobuf than I expected.</p>
<a href="https://github.com/sudorandom/protojsonx" target="_blank" rel="noopener noreferrer" class="github-repo-card">
  <div class="github-repo-card-header">
    <div class="github-repo-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg></div>
    <span class="github-repo-name">sudorandom/protojsonx</span>
  </div><p class="github-repo-description">An experimental faster ProtoJSON encoder and decoder for Go.</p></a>

<blockquote>
<p><strong>Warning:</strong> <code>protojsonx</code> is highly experimental at this stage. It does pass the official Protobuf conformance tests, which gives me confidence that it follows the expected ProtoJSON behavior, but I would still treat it as early-stage software.</p>
</blockquote>
<hr>
<h2 id="what-is-protojsonx">What is protojsonx?</h2>
<p><a href="https://github.com/sudorandom/protojsonx" rel="external">protojsonx</a> is designed as a mostly API-compatible experimental replacement for Go&rsquo;s official <code>google.golang.org/protobuf/encoding/protojson</code> library. It supports core ProtoJSON behaviors like field names, enums, presence semantics, unknown-field handling, <code>json_name</code>, oneofs, maps, and standard marshaling/unmarshaling options. However, it does not yet cover every edge case or configuration option of the official library (such as dynamic <code>Any</code> resolving).</p>
<p>To make serialization faster, it implements two key optimization strategies:</p>
<ul>
<li><strong>Runtime Table Mode</strong>: This mode implements the startup compilation strategy. It builds flat offset tables at initialization, allowing it to marshal and unmarshal structures using fast, sequential offset arithmetic instead of per-call descriptor traversal.</li>
<li><strong>Generated Plugin Mode</strong>: For the highest-performance path, <code>protojsonx</code> provides a protoc plugin (<code>protoc-gen-go-protojsonx</code>) that generates type-specific marshaling and unmarshaling methods directly. At that point, a lot of the runtime bookkeeping disappears. The remaining cost is less about “what field is this?” and more about the unavoidable work of reading or writing JSON.</li>
</ul>
<hr>
<h2 id="the-benchmark-setup">The Benchmark Setup</h2>
<p>I used the benchmark suite from my previous article, running on Go 1.26 on an Apple M1 Pro. The configurations are:</p>
<ul>
<li><strong>Small:</strong> A flat object with 4 fields (string ID, status boolean, age integer, score float).</li>
<li><strong>Medium:</strong> A nested user signup event containing actor object, string tags, and metadata map.</li>
<li><strong>Large:</strong> An array repeating the Medium object 100 times.</li>
</ul>
<h3 id="the-rules-of-the-game">The Rules of the Game</h3>
<p>All tests use equivalent payload shapes and measure end-to-end marshal/unmarshal cost, including allocations. The generic JSON cases use plain Go structs with similar fields, while the protobuf cases use generated protobuf messages.</p>
<p>This is not a perfect apples-to-apples comparison. ProtoJSON has extra rules around field names, presence, enums, well-known types, and numeric encoding. The point is narrower: if you already need ProtoJSON-shaped output, how much does that cost compared with the JSON tools Go developers normally reach for? These benchmarks compare the cost of serving similar application-shaped JSON payloads, not identical semantics.</p>
<p>I also included <code>hyperpb</code>, a descriptor/layout-driven dynamic protobuf parser built around read-oriented offset decoding, including <code>hyperpb.Shared</code> where the benchmark can reuse its arena. It is not a ProtoJSON library, so its numbers should be read as a binary protobuf reference point rather than a direct competitor.</p>
<h3 id="payload-sizes">Payload Sizes</h3>
<p>Serialization output size matters, especially when comparing JSON and binary protobuf. These are the payload sizes produced by the benchmark fixtures:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Payload</th>
          <th style="text-align: right">ProtoJSON bytes</th>
          <th style="text-align: right">generic JSON bytes</th>
          <th style="text-align: right">binary proto bytes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Small</td>
          <td style="text-align: right">55 B</td>
          <td style="text-align: right">55 B</td>
          <td style="text-align: right">25 B</td>
      </tr>
      <tr>
          <td style="text-align: left">Medium</td>
          <td style="text-align: right">293 B</td>
          <td style="text-align: right">291 B</td>
          <td style="text-align: right">162 B</td>
      </tr>
      <tr>
          <td style="text-align: left">Large</td>
          <td style="text-align: right">29,412 B</td>
          <td style="text-align: right">29,201 B</td>
          <td style="text-align: right">16,500 B</td>
      </tr>
  </tbody>
</table>
<p>The ProtoJSON and generic JSON sizes are close, but not byte-identical. Binary protobuf is much smaller for these payloads, so the binary rows should be read as a compact binary reference point rather than a JSON-format comparison.</p>
<h3 id="headline-results">Headline Results</h3>
<p>Before the full tables, here is the short version for the fastest JSON path in each group:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Operation</th>
          <th style="text-align: left">Best JSON option</th>
          <th style="text-align: right">Compared with official protojson</th>
          <th style="text-align: right">Compared with encoding/json</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Small marshal</td>
          <td style="text-align: left">protojsonx generated</td>
          <td style="text-align: right">6.0x faster</td>
          <td style="text-align: right">1.8x faster</td>
      </tr>
      <tr>
          <td style="text-align: left">Medium marshal</td>
          <td style="text-align: left">protojsonx generated</td>
          <td style="text-align: right">7.0x faster</td>
          <td style="text-align: right">1.6x faster</td>
      </tr>
      <tr>
          <td style="text-align: left">Large marshal</td>
          <td style="text-align: left">protojsonx generated</td>
          <td style="text-align: right">8.1x faster</td>
          <td style="text-align: right">1.5x faster</td>
      </tr>
      <tr>
          <td style="text-align: left">Small unmarshal</td>
          <td style="text-align: left">protojsonx generated</td>
          <td style="text-align: right">6.9x faster</td>
          <td style="text-align: right">5.5x faster</td>
      </tr>
      <tr>
          <td style="text-align: left">Medium unmarshal</td>
          <td style="text-align: left">protojsonx generated</td>
          <td style="text-align: right">5.1x faster</td>
          <td style="text-align: right">4.1x faster</td>
      </tr>
      <tr>
          <td style="text-align: left">Large unmarshal</td>
          <td style="text-align: left">protojsonx generated</td>
          <td style="text-align: right">5.1x faster</td>
          <td style="text-align: right">3.7x faster</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="marshaling-performance">Marshaling Performance</h2>
<p>Marshaling is the happier path here. The encoder already has typed Go values in memory, so the main question is how quickly each implementation can walk those values and write JSON bytes.</p>
<div class="rss-tabs-container">
  
  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Small Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="a1955001fe172712" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('a1955001fe172712');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "protojson",
      "encoding/json",
      "encoding/json/v2",
      "protojsonx (Runtime Tables)",
      "protojsonx (Generated Plugin)",
      "proto.Marshal",
      "vtproto",
      "hyperpb + Shared"
    ],
    "datasets": [
      {
        "label": "Small Marshal (ns/op)",
        "data": [
          612,
          183,
          289,
          142,
          102,
          83,
          25,
          296
        ],
        "backgroundColor": [
          "rgba(186, 85, 211, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 130, 0, 0.75)",
          "rgba(135, 206, 250, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(50, 205, 50, 0.75)",
          "rgba(0, 250, 154, 0.75)",
          "rgba(34, 139, 34, 0.75)"
        ],
        "borderColor": [
          "rgba(186, 85, 211, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 130, 0, 1)",
          "rgba(135, 206, 250, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(50, 205, 50, 1)",
          "rgba(0, 250, 154, 1)",
          "rgba(34, 139, 34, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Marshaling Performance (Small Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "display": false
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": {
          "color": "#fff"
        }
      },
      "y": {
        "ticks": {
          "color": "#fff"
        }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show complete data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Serializer</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
          <th style="text-align: center">Speed vs protojson</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>protojson</strong></td>
          <td style="text-align: center">612 ns</td>
          <td style="text-align: center">512 B</td>
          <td style="text-align: center">12</td>
          <td style="text-align: center">1.0x (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json</strong></td>
          <td style="text-align: center">183 ns</td>
          <td style="text-align: center">64 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json/v2</strong></td>
          <td style="text-align: center">289 ns</td>
          <td style="text-align: center">112 B</td>
          <td style="text-align: center">2</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Runtime Tables)</strong></td>
          <td style="text-align: center"><strong>142 ns</strong></td>
          <td style="text-align: center"><strong>64 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
          <td style="text-align: center"><strong>4.3x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Generated Plugin)</strong></td>
          <td style="text-align: center"><strong>102 ns</strong></td>
          <td style="text-align: center"><strong>64 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
          <td style="text-align: center"><strong>6.0x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>proto.Marshal</strong></td>
          <td style="text-align: center">83 ns</td>
          <td style="text-align: center">32 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>vtproto</strong></td>
          <td style="text-align: center">25 ns</td>
          <td style="text-align: center">32 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb + Shared</strong></td>
          <td style="text-align: center">296 ns</td>
          <td style="text-align: center">144 B</td>
          <td style="text-align: center">7</td>
          <td style="text-align: center">-</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Medium Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="834e2c757c17c4f3" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('834e2c757c17c4f3');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "protojson",
      "encoding/json",
      "encoding/json/v2",
      "protojsonx (Runtime Tables)",
      "protojsonx (Generated Plugin)",
      "proto.Marshal",
      "vtproto",
      "hyperpb + Shared"
    ],
    "datasets": [
      {
        "label": "Medium Marshal (ns/op)",
        "data": [
          2227,
          521,
          803,
          404,
          318,
          285,
          103,
          1062
        ],
        "backgroundColor": [
          "rgba(186, 85, 211, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 130, 0, 0.75)",
          "rgba(135, 206, 250, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(50, 205, 50, 0.75)",
          "rgba(0, 250, 154, 0.75)",
          "rgba(34, 139, 34, 0.75)"
        ],
        "borderColor": [
          "rgba(186, 85, 211, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 130, 0, 1)",
          "rgba(135, 206, 250, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(50, 205, 50, 1)",
          "rgba(0, 250, 154, 1)",
          "rgba(34, 139, 34, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Marshaling Performance (Medium Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "display": false
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": {
          "color": "#fff"
        }
      },
      "y": {
        "ticks": {
          "color": "#fff"
        }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show complete data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Serializer</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
          <th style="text-align: center">Speed vs protojson</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>protojson</strong></td>
          <td style="text-align: center">2,227 ns</td>
          <td style="text-align: center">1,722 B</td>
          <td style="text-align: center">34</td>
          <td style="text-align: center">1.0x (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json</strong></td>
          <td style="text-align: center">521 ns</td>
          <td style="text-align: center">464 B</td>
          <td style="text-align: center">2</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json/v2</strong></td>
          <td style="text-align: center">803 ns</td>
          <td style="text-align: center">608 B</td>
          <td style="text-align: center">3</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Runtime Tables)</strong></td>
          <td style="text-align: center"><strong>404 ns</strong></td>
          <td style="text-align: center"><strong>320 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
          <td style="text-align: center"><strong>5.5x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Generated Plugin)</strong></td>
          <td style="text-align: center"><strong>318 ns</strong></td>
          <td style="text-align: center"><strong>320 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
          <td style="text-align: center"><strong>7.0x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>proto.Marshal</strong></td>
          <td style="text-align: center">285 ns</td>
          <td style="text-align: center">176 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>vtproto</strong></td>
          <td style="text-align: center">103 ns</td>
          <td style="text-align: center">176 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb + Shared</strong></td>
          <td style="text-align: center">1,062 ns</td>
          <td style="text-align: center">744 B</td>
          <td style="text-align: center">17</td>
          <td style="text-align: center">-</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Large Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="173104d840bf5127" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('173104d840bf5127');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "protojson",
      "encoding/json",
      "encoding/json/v2",
      "protojsonx (Runtime Tables)",
      "protojsonx (Generated Plugin)",
      "proto.Marshal",
      "vtproto",
      "hyperpb + Shared"
    ],
    "datasets": [
      {
        "label": "Large Marshal (ns/op)",
        "data": [
          223838,
          41250,
          62036,
          31134,
          27601,
          25131,
          7575,
          103495
        ],
        "backgroundColor": [
          "rgba(186, 85, 211, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 130, 0, 0.75)",
          "rgba(135, 206, 250, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(50, 205, 50, 0.75)",
          "rgba(0, 250, 154, 0.75)",
          "rgba(34, 139, 34, 0.75)"
        ],
        "borderColor": [
          "rgba(186, 85, 211, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 130, 0, 1)",
          "rgba(135, 206, 250, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(50, 205, 50, 1)",
          "rgba(0, 250, 154, 1)",
          "rgba(34, 139, 34, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Marshaling Performance (Large Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "display": false
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": {
          "color": "#fff"
        }
      },
      "y": {
        "ticks": {
          "color": "#fff"
        }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show complete data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Serializer</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
          <th style="text-align: center">Speed vs protojson</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>protojson</strong></td>
          <td style="text-align: center">223,838 ns</td>
          <td style="text-align: center">243,749 B</td>
          <td style="text-align: center">2728</td>
          <td style="text-align: center">1.0x (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json</strong></td>
          <td style="text-align: center">41,250 ns</td>
          <td style="text-align: center">32,823 B</td>
          <td style="text-align: center">2</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json/v2</strong></td>
          <td style="text-align: center">62,036 ns</td>
          <td style="text-align: center">32,856 B</td>
          <td style="text-align: center">3</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Runtime Tables)</strong></td>
          <td style="text-align: center"><strong>31,134 ns</strong></td>
          <td style="text-align: center"><strong>32,797 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
          <td style="text-align: center"><strong>7.2x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Generated Plugin)</strong></td>
          <td style="text-align: center"><strong>27,601 ns</strong></td>
          <td style="text-align: center"><strong>32,784 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
          <td style="text-align: center"><strong>8.1x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>proto.Marshal</strong></td>
          <td style="text-align: center">25,131 ns</td>
          <td style="text-align: center">18,432 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>vtproto</strong></td>
          <td style="text-align: center">7,575 ns</td>
          <td style="text-align: center">18,432 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb + Shared</strong></td>
          <td style="text-align: center">103,495 ns</td>
          <td style="text-align: center">107,216 B</td>
          <td style="text-align: center">1022</td>
          <td style="text-align: center">-</td>
      </tr>
  </tbody>
</table>
</details>

</div>


</div>

<h3 id="takeaways-marshaling">Takeaways: Marshaling</h3>
<p>Most of the marshaling performance gain comes from compiling descriptors up-front. Standard <code>protojson</code> spends its cycles querying Go’s reflection and descriptor trees on the fly, creating a continuous stream of allocations. Compiling those layout mappings once at startup (<strong>Runtime Table Mode</strong>) allows the serialization process to bypass runtime lookup loops entirely, converting the fields to JSON via sequential offset math.</p>
<p>The generated plugin goes one step further. Instead of building layout tables at startup, it writes the field access code directly into the generated Go file. That removes another layer of lookup and assertion work from the hot path.</p>
<p>The tradeoff is the usual one for generated code: in a large schema repository, generating specialized JSON routines for hundreds of message types can increase compiled binary size.</p>
<p>For these fixtures, schema-guided ProtoJSON marshaling beats both <code>encoding/json</code> and <code>encoding/json/v2</code> (<code>github.com/go-json-experiment/json</code>), and the generated encoder lands close to standard binary <code>proto.Marshal</code>. <code>hyperpb + Shared</code> is slower on marshal here, which fits its design: it is much more interesting as a read-oriented parser than as a serializer.</p>
<hr>
<h2 id="unmarshaling-performance">Unmarshaling Performance</h2>
<p>Unmarshaling is the harder side of the benchmark. The decoder has to turn strings, tokens, object keys, maps, and repeated fields back into typed Go values. The tables below show both the runtime-table path and the generated path.</p>
<div class="rss-tabs-container">
  
  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Small Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="035726e4753790eb" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('035726e4753790eb');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "protojson",
      "encoding/json",
      "encoding/json/v2",
      "protojsonx (Runtime Tables)",
      "protojsonx (Generated Plugin)",
      "proto.Unmarshal",
      "vtproto",
      "hyperpb",
      "hyperpb + Shared"
    ],
    "datasets": [
      {
        "label": "Small Unmarshal (ns/op)",
        "data": [
          934,
          752,
          339,
          267,
          136,
          114,
          25,
          360,
          125
        ],
        "backgroundColor": [
          "rgba(186, 85, 211, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 130, 0, 0.75)",
          "rgba(135, 206, 250, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(50, 205, 50, 0.75)",
          "rgba(0, 250, 154, 0.75)",
          "rgba(34, 139, 34, 0.75)",
          "rgba(34, 139, 34, 0.45)"
        ],
        "borderColor": [
          "rgba(186, 85, 211, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 130, 0, 1)",
          "rgba(135, 206, 250, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(50, 205, 50, 1)",
          "rgba(0, 250, 154, 1)",
          "rgba(34, 139, 34, 1)",
          "rgba(34, 139, 34, 0.8)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Unmarshaling Performance (Small Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "display": false
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": {
          "color": "#fff"
        }
      },
      "y": {
        "ticks": {
          "color": "#fff"
        }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show complete data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Serializer</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
          <th style="text-align: center">Speed vs protojson</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>protojson</strong></td>
          <td style="text-align: center">934 ns</td>
          <td style="text-align: center">336 B</td>
          <td style="text-align: center">14</td>
          <td style="text-align: center">1.0x (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json</strong></td>
          <td style="text-align: center">752 ns</td>
          <td style="text-align: center">280 B</td>
          <td style="text-align: center">6</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json/v2</strong></td>
          <td style="text-align: center">339 ns</td>
          <td style="text-align: center">48 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Runtime Tables)</strong></td>
          <td style="text-align: center"><strong>267 ns</strong></td>
          <td style="text-align: center"><strong>96 B</strong></td>
          <td style="text-align: center"><strong>3</strong></td>
          <td style="text-align: center"><strong>3.5x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Generated Plugin)</strong></td>
          <td style="text-align: center"><strong>136 ns</strong></td>
          <td style="text-align: center"><strong>96 B</strong></td>
          <td style="text-align: center"><strong>2</strong></td>
          <td style="text-align: center"><strong>6.9x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>proto.Unmarshal</strong></td>
          <td style="text-align: center">114 ns</td>
          <td style="text-align: center">96 B</td>
          <td style="text-align: center">2</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>vtproto</strong></td>
          <td style="text-align: center">25 ns</td>
          <td style="text-align: center">16 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb</strong></td>
          <td style="text-align: center">360 ns</td>
          <td style="text-align: center">798 B</td>
          <td style="text-align: center">4</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb + Shared</strong></td>
          <td style="text-align: center">125 ns</td>
          <td style="text-align: center">65 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Medium Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="30f11b99287d0ca9" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('30f11b99287d0ca9');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "protojson",
      "encoding/json",
      "encoding/json/v2",
      "protojsonx (Runtime Tables)",
      "protojsonx (Generated Plugin)",
      "proto.Unmarshal",
      "vtproto",
      "hyperpb",
      "hyperpb + Shared"
    ],
    "datasets": [
      {
        "label": "Medium Unmarshal (ns/op)",
        "data": [
          3703,
          2937,
          1133,
          1108,
          720,
          571,
          322,
          638,
          290
        ],
        "backgroundColor": [
          "rgba(186, 85, 211, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 130, 0, 0.75)",
          "rgba(135, 206, 250, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(50, 205, 50, 0.75)",
          "rgba(0, 250, 154, 0.75)",
          "rgba(34, 139, 34, 0.75)",
          "rgba(34, 139, 34, 0.45)"
        ],
        "borderColor": [
          "rgba(186, 85, 211, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 130, 0, 1)",
          "rgba(135, 206, 250, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(50, 205, 50, 1)",
          "rgba(0, 250, 154, 1)",
          "rgba(34, 139, 34, 1)",
          "rgba(34, 139, 34, 0.8)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Unmarshaling Performance (Medium Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "display": false
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": {
          "color": "#fff"
        }
      },
      "y": {
        "ticks": {
          "color": "#fff"
        }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show complete data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Serializer</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
          <th style="text-align: center">Speed vs protojson</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>protojson</strong></td>
          <td style="text-align: center">3,703 ns</td>
          <td style="text-align: center">1,304 B</td>
          <td style="text-align: center">58</td>
          <td style="text-align: center">1.0x (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json</strong></td>
          <td style="text-align: center">2,937 ns</td>
          <td style="text-align: center">688 B</td>
          <td style="text-align: center">19</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json/v2</strong></td>
          <td style="text-align: center">1,133 ns</td>
          <td style="text-align: center">256 B</td>
          <td style="text-align: center">4</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Runtime Tables)</strong></td>
          <td style="text-align: center"><strong>1,108 ns</strong></td>
          <td style="text-align: center"><strong>576 B</strong></td>
          <td style="text-align: center"><strong>16</strong></td>
          <td style="text-align: center"><strong>3.3x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Generated Plugin)</strong></td>
          <td style="text-align: center"><strong>720 ns</strong></td>
          <td style="text-align: center"><strong>528 B</strong></td>
          <td style="text-align: center"><strong>14</strong></td>
          <td style="text-align: center"><strong>5.1x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>proto.Unmarshal</strong></td>
          <td style="text-align: center">571 ns</td>
          <td style="text-align: center">560 B</td>
          <td style="text-align: center">15</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>vtproto</strong></td>
          <td style="text-align: center">322 ns</td>
          <td style="text-align: center">432 B</td>
          <td style="text-align: center">14</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb</strong></td>
          <td style="text-align: center">638 ns</td>
          <td style="text-align: center">1,446 B</td>
          <td style="text-align: center">5</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb + Shared</strong></td>
          <td style="text-align: center">290 ns</td>
          <td style="text-align: center">357 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Large Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="d2185111d4b97250" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('d2185111d4b97250');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "protojson",
      "encoding/json",
      "encoding/json/v2",
      "protojsonx (Runtime Tables)",
      "protojsonx (Generated Plugin)",
      "proto.Unmarshal",
      "vtproto",
      "hyperpb",
      "hyperpb + Shared"
    ],
    "datasets": [
      {
        "label": "Large Unmarshal (ns/op)",
        "data": [
          377892,
          272103,
          108811,
          111141,
          73389,
          54923,
          36002,
          24835,
          18163
        ],
        "backgroundColor": [
          "rgba(186, 85, 211, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 130, 0, 0.75)",
          "rgba(135, 206, 250, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(50, 205, 50, 0.75)",
          "rgba(0, 250, 154, 0.75)",
          "rgba(34, 139, 34, 0.75)",
          "rgba(34, 139, 34, 0.45)"
        ],
        "borderColor": [
          "rgba(186, 85, 211, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 130, 0, 1)",
          "rgba(135, 206, 250, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(50, 205, 50, 1)",
          "rgba(0, 250, 154, 1)",
          "rgba(34, 139, 34, 1)",
          "rgba(34, 139, 34, 0.8)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Unmarshaling Performance (Large Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "display": false
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": {
          "color": "#fff"
        }
      },
      "y": {
        "ticks": {
          "color": "#fff"
        }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show complete data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Serializer</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
          <th style="text-align: center">Speed vs protojson</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>protojson</strong></td>
          <td style="text-align: center">377,892 ns</td>
          <td style="text-align: center">119,256 B</td>
          <td style="text-align: center">5713</td>
          <td style="text-align: center">1.0x (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json</strong></td>
          <td style="text-align: center">272,103 ns</td>
          <td style="text-align: center">70,584 B</td>
          <td style="text-align: center">1216</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>encoding/json/v2</strong></td>
          <td style="text-align: center">108,811 ns</td>
          <td style="text-align: center">54,305 B</td>
          <td style="text-align: center">309</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Runtime Tables)</strong></td>
          <td style="text-align: center"><strong>111,141 ns</strong></td>
          <td style="text-align: center"><strong>62,232 B</strong></td>
          <td style="text-align: center"><strong>1709</strong></td>
          <td style="text-align: center"><strong>3.4x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>protojsonx (Generated Plugin)</strong></td>
          <td style="text-align: center"><strong>73,389 ns</strong></td>
          <td style="text-align: center"><strong>55,008 B</strong></td>
          <td style="text-align: center"><strong>1407</strong></td>
          <td style="text-align: center"><strong>5.1x faster</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>proto.Unmarshal</strong></td>
          <td style="text-align: center">54,923 ns</td>
          <td style="text-align: center">58,232 B</td>
          <td style="text-align: center">1509</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>vtproto</strong></td>
          <td style="text-align: center">36,002 ns</td>
          <td style="text-align: center">58,168 B</td>
          <td style="text-align: center">1508</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb</strong></td>
          <td style="text-align: center">24,835 ns</td>
          <td style="text-align: center">60,053 B</td>
          <td style="text-align: center">12</td>
          <td style="text-align: center">-</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>hyperpb + Shared</strong></td>
          <td style="text-align: center">18,163 ns</td>
          <td style="text-align: center">21,863 B</td>
          <td style="text-align: center">1</td>
          <td style="text-align: center">-</td>
      </tr>
  </tbody>
</table>
</details>

</div>


</div>

<h3 id="takeaways-unmarshaling">Takeaways: Unmarshaling</h3>
<p>Decode is where the generated code earns its keep. If you’ve ever looked at Go’s standard <code>protojson</code> decoder, it spends a lot of time matching string keys against descriptors, allocating intermediate maps, and resolving types at runtime. Compiling direct field assignments into the generated decoder cuts out a lot of that work: 5.1x to 6.9x faster than official <code>protojson</code> on these static payloads. It also reduces heap churn substantially; unmarshaling the Large payload drops from over 5,700 allocations down to just 1,407.</p>
<p>The interesting part is that the generated JSON decoder beats both <code>encoding/json</code> and <code>encoding/json/v2</code> (<code>go-json-experiment/json</code>) in these fixtures. It still does not catch binary protobuf, but it gets close enough that ProtoJSON stops looking like an automatic performance disaster.</p>
<p>That said, unmarshaling still has a wider performance gap relative to binary Protobuf than marshaling does. This is a fundamental constraint of the JSON format itself. A binary decoder can skip fields using length prefixes and parse integer types with very little overhead. A JSON decoder, by contrast, is forced to parse string layouts, handle token boundaries, and instantiate nested objects and map fields on the fly. But even with these structural constraints, the reflection-free generated code narrows the gap significantly, turning what used to be a large bottleneck into a much smaller and more predictable cost.</p>
<hr>
<h2 id="how-protojsonx-compares-to-generic-json--binary-formats">How protojsonx Compares to Generic JSON &amp; Binary Formats</h2>
<p>The basic idea held up: <strong>moving schema work out of the hot path can make ProtoJSON faster than Go&rsquo;s standard library <code>encoding/json</code> package for these fixtures.</strong></p>
<p>The external <strong><code>github.com/go-json-experiment/json</code> package (the prototype implementation for <code>encoding/json/v2</code>) also looks very strong.</strong> Under unmarshaling, it cuts a lot of latency and allocation overhead compared with <code>encoding/json</code> (e.g. unmarshaling the Large payload drops from 272,103 ns/op down to 108,811 ns/op).</p>
<p>The experimental <code>encoding/json/v2</code> package gets close to <code>protojsonx</code> Runtime Table Mode, but the generated <code>protojsonx</code> path was still the fastest JSON option in these benchmarks.</p>
<p>A rough summary of the benchmark results looks like this:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Serializer</th>
          <th style="text-align: left">Role in these benchmarks</th>
          <th style="text-align: left">Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Official <code>protojson</code></strong></td>
          <td style="text-align: left">Slowest JSON path</td>
          <td style="text-align: left">Most general and most dynamic</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong><code>encoding/json</code></strong></td>
          <td style="text-align: left">Standard JSON baseline</td>
          <td style="text-align: left">General-purpose reflection-based JSON</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong><code>encoding/json/v2</code> (<code>go-json-experiment/json</code>)</strong></td>
          <td style="text-align: left">Faster generic JSON baseline</td>
          <td style="text-align: left">Official v2 prototype; much better on unmarshal</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong><code>protojsonx</code> Runtime Table Mode</strong></td>
          <td style="text-align: left">Fast dynamic ProtoJSON</td>
          <td style="text-align: left">Descriptor work moved to startup</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong><code>protojsonx</code> Generated Plugin Mode</strong></td>
          <td style="text-align: left">Fastest JSON path here</td>
          <td style="text-align: left">Type-specific generated code</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Standard binary <code>proto</code></strong></td>
          <td style="text-align: left">Binary protobuf baseline</td>
          <td style="text-align: left">Compact protobuf wire format</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong><code>vtproto</code></strong></td>
          <td style="text-align: left">Fast generated binary baseline</td>
          <td style="text-align: left">Strongest generated marshal path here</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong><code>hyperpb + Shared</code></strong></td>
          <td style="text-align: left">Binary decode reference</td>
          <td style="text-align: left">Very fast reusable-storage decode path</td>
      </tr>
  </tbody>
</table>
<p>For static-message marshaling, <code>protojsonx</code> with the plugin gets much closer to standard binary Protobuf than official <code>protojson</code>, and in these benchmarks it beats generic <code>encoding/json</code> and <code>encoding/json/v2</code> (<code>go-json-experiment/json</code>).</p>
<hr>
<h2 id="methodology">Methodology</h2>
<p>To ensure these results are reproducible, here are the environment parameters and test details used for this benchmark run:</p>
<ul>
<li><strong>Go Version</strong>: <code>go version go1.26.3 darwin/arm64</code></li>
<li><strong>Machine Details</strong>: Apple M1 Pro (10-core CPU, 16GB unified memory, macOS)</li>
<li><strong>Target Library Version</strong>: <code>github.com/sudorandom/protojsonx@v0.0.6</code></li>
<li><strong>Benchmark Commit</strong>: <code>b8fff78c</code></li>
<li><strong>Benchmark Source</strong>: The benchmark code is available in the <a href="https://github.com/sudorandom/kmcd.dev/tree/main/content/posts/2026/benchmarking-protojsonx/benchmarks/" rel="external">benchmarks/</a> folder.</li>
<li><strong>Test Execution Command</strong>:
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go <span style="color:#81a1c1">test</span> -bench<span style="color:#81a1c1">=</span>. -benchmem -benchtime<span style="color:#81a1c1">=</span>5s -count<span style="color:#81a1c1">=</span><span style="color:#b48ead">5</span> &gt; results.txt
</span></span></code></pre></div></li>
</ul>
<p>All benchmarks were run on an otherwise idle machine using a multi-run sequence to reduce noise. All runs were performed on an AC-powered, thermally-settled machine to prevent thermal throttling or low-power state interference. The command writes the five raw runs for each benchmark to <code>results.txt</code>; the article tables report arithmetic means for <code>ns/op</code>, <code>B/op</code>, and <code>allocs/op</code> computed from those raw rows. For stricter statistical comparison, I would increase the run count and summarize with <code>benchstat</code>.</p>
<p>I left <code>GOMAXPROCS</code> at Go’s default for this machine, which was 8.</p>
<p>As always with microbenchmarks, the exact numbers matter less than the overall shape of the results. You should benchmark your own schemas and payloads under production-realistic environments before drawing final design conclusions.</p>
<hr>
<h2 id="try-it-out--give-feedback">Try it Out &amp; Give Feedback!</h2>
<p><code>protojsonx</code> is still highly experimental and early-stage software. It passes the official Protobuf conformance tests, which gives me confidence that the core serialization and parsing behavior is on the right track, but I would not treat it as boring infrastructure yet.</p>
<p>If you are serving JSON APIs backed by Protobuf and <code>protojson</code> is showing up in your profiles, I’d love for you to try it against your own schemas.</p>
<p>I’m especially interested in results from real production-shaped messages: large repeated fields, maps, oneofs, well-known types, custom <code>json_name</code> usage, and other cases that are more interesting than tiny benchmark fixtures.</p>
<p>You can find the code and instructions on GitHub:</p>
<ul>
<li><strong>GitHub Repository</strong>: <a href="https://github.com/sudorandom/protojsonx" rel="external">sudorandom/protojsonx</a></li>
</ul>
<p>Please file issues or share benchmark results there. The more weird schemas, the better.</p>
]]></content:encoded></item></channel></rss>