<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Anh Quân Nguyễn</title>
    <description>The latest articles on Forem by Anh Quân Nguyễn (@anh_qunnguyn_57549060f).</description>
    <link>https://forem.com/anh_qunnguyn_57549060f</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2189853%2F405c0fa0-9bd1-48d2-b27c-9ad1801775c7.jpg</url>
      <title>Forem: Anh Quân Nguyễn</title>
      <link>https://forem.com/anh_qunnguyn_57549060f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/anh_qunnguyn_57549060f"/>
    <language>en</language>
    <item>
      <title>JSON Schema in 10 Minutes — Validation, Types &amp; Real Examples</title>
      <dc:creator>Anh Quân Nguyễn</dc:creator>
      <pubDate>Tue, 26 May 2026 03:44:14 +0000</pubDate>
      <link>https://forem.com/anh_qunnguyn_57549060f/json-schema-in-10-minutes-validation-types-real-examples-4pjg</link>
      <guid>https://forem.com/anh_qunnguyn_57549060f/json-schema-in-10-minutes-validation-types-real-examples-4pjg</guid>
      <description>&lt;p&gt;Two years ago I shipped a webhook handler without input validation. A partner started sending us a slightly malformed payload (an extra field, one missing required field) and our worker silently processed garbage into the database for three days before anyone noticed. By the time I traced it, we had 12,000 corrupt rows and a very awkward customer call.&lt;/p&gt;

&lt;p&gt;I learned JSON Schema the next week. This post is the cheat sheet I wish someone had handed me on day one — the keywords I actually use, the gotchas that bit me again later, and the honest comparison with OpenAPI and TypeScript types.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven types you'll use
&lt;/h2&gt;

&lt;p&gt;Every JSON value is one of seven types: &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;object&lt;/code&gt;, &lt;code&gt;array&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt;. The &lt;code&gt;integer&lt;/code&gt; type is a JSON Schema convenience (raw JSON only has &lt;code&gt;number&lt;/code&gt;) but the schema layer enforces "no decimal places." A minimal schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That validates any string and rejects everything else. You can also accept a union:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"null"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful for optional fields you want to keep present in the payload rather than omitting. Before I write more than a one-line schema I usually paste a sample payload into a &lt;a href="https://calculators.im/json-formatter" rel="noopener noreferrer"&gt;JSON formatter&lt;/a&gt; to see the actual shape pretty-printed. Type errors almost always come from misreading the structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Objects, required, and the additionalProperties trap
&lt;/h2&gt;

&lt;p&gt;Most real validation work happens on objects. The three keywords you use every day are &lt;code&gt;properties&lt;/code&gt;, &lt;code&gt;required&lt;/code&gt;, and &lt;code&gt;additionalProperties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three gotchas to internalize. First, &lt;code&gt;properties&lt;/code&gt; describes each field but does NOT make any of them required. Without the &lt;code&gt;required&lt;/code&gt; array, every property is optional. Second, &lt;code&gt;required&lt;/code&gt; is a separate list of property names that must be present (presence only, you still need &lt;code&gt;type&lt;/code&gt; to validate the value). Third, &lt;code&gt;additionalProperties: false&lt;/code&gt; rejects any property not listed. Without this line, the schema accepts arbitrary extra fields silently. This was the bug that hit me — the partner was sending &lt;code&gt;email_address&lt;/code&gt; instead of &lt;code&gt;email&lt;/code&gt;, and without &lt;code&gt;additionalProperties: false&lt;/code&gt; my schema accepted it as "no email + an unknown field."&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;additionalProperties: false&lt;/code&gt; by default. Remove it only when you genuinely want a free-form object. For maps with arbitrary keys but a known value type, use it as a schema instead of a boolean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That validates any object where every value is a number. Perfect for price lookup tables, feature-flag percentages, or anything keyed dynamically.&lt;/p&gt;

&lt;h2&gt;
  
  
  String validation: minLength, pattern, format, enum
&lt;/h2&gt;

&lt;p&gt;Real string validation goes beyond "is it a string." The keywords that earn their keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;minLength&lt;/code&gt; / &lt;code&gt;maxLength&lt;/code&gt;, integer bounds on UTF-16 code units (not bytes, not graphemes)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pattern&lt;/code&gt;, ECMA-262 regex the string must match somewhere (use &lt;code&gt;^...$&lt;/code&gt; anchors for a full match)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;format&lt;/code&gt;, named formats like &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;uri&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;date-time&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;ipv4&lt;/code&gt;, &lt;code&gt;ipv6&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enum&lt;/code&gt;, a fixed list of allowed values (works for any type)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;const&lt;/code&gt;, a single allowed value (equivalent to a one-item enum)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical username field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^[a-zA-Z0-9_]+$"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha that cost me a day: &lt;code&gt;format&lt;/code&gt; is informational by default in older drafts. You must enable format assertion in your validator. Ajv requires &lt;code&gt;ajv-formats&lt;/code&gt;. Python &lt;code&gt;jsonschema&lt;/code&gt; needs &lt;code&gt;format_checker&lt;/code&gt;. Without it, &lt;code&gt;"format": "email"&lt;/code&gt; documents intent but does not actually reject invalid emails. See the &lt;a href="https://json-schema.org/understanding-json-schema/reference/string#format" rel="noopener noreferrer"&gt;JSON Schema spec for format&lt;/a&gt; for the full list and the assertion behavior per draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Number validation: minimum, maximum, multipleOf
&lt;/h2&gt;

&lt;p&gt;For numbers and integers, the validation keywords are arithmetic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;minimum&lt;/code&gt; / &lt;code&gt;maximum&lt;/code&gt;, inclusive bounds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exclusiveMinimum&lt;/code&gt; / &lt;code&gt;exclusiveMaximum&lt;/code&gt;, exclusive bounds (in Draft 2020-12 these take a number, in older drafts they took a boolean)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;multipleOf&lt;/code&gt;, the value must be a multiple of this number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validating a percentage that must be 0 to 100 in 0.01 increments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"multipleOf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;multipleOf&lt;/code&gt; has a floating-point trap I keep getting wrong. 0.1 is not exactly representable in IEEE 754, so &lt;code&gt;{ "multipleOf": 0.1 }&lt;/code&gt; will sometimes reject values you expect to pass. For money, I now store and validate as integer cents (&lt;code&gt;{ "type": "integer", "minimum": 0 }&lt;/code&gt;). It is the same precision argument behind storing prices in the smallest currency unit everywhere else in the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Array validation: items, minItems, uniqueItems
&lt;/h2&gt;

&lt;p&gt;For arrays the workhorses are &lt;code&gt;items&lt;/code&gt; (schema applied to every element), &lt;code&gt;minItems&lt;/code&gt; / &lt;code&gt;maxItems&lt;/code&gt; (length bounds), and &lt;code&gt;uniqueItems&lt;/code&gt; (rejects duplicates by deep equality). A list of unique tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uniqueItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For positional tuples where each index has a different schema, use &lt;code&gt;prefixItems&lt;/code&gt; in Draft 2020-12 or &lt;code&gt;items&lt;/code&gt; as an array in older drafts. A coordinate pair where index 0 is longitude and index 1 is latitude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prefixItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;-90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trailing &lt;code&gt;"items": false&lt;/code&gt; rejects any extra elements beyond the two declared positions. The array equivalent of &lt;code&gt;additionalProperties: false&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema composition: $ref, allOf, oneOf, anyOf
&lt;/h2&gt;

&lt;p&gt;Once your schemas grow past a single page, you will want to break them up and combine them. JSON Schema has four composition keywords:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$ref&lt;/code&gt;, reuse another schema by JSON Pointer (e.g., &lt;code&gt;"#/$defs/address"&lt;/code&gt; or an external URL)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allOf&lt;/code&gt;, data must validate against every subschema (intersection / mixin)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;anyOf&lt;/code&gt;, data must validate against at least one (union, OK if multiple match)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;oneOf&lt;/code&gt;, data must validate against exactly one (XOR, rejects if zero or multiple match)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reusable address schema referenced from two parents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$defs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"street"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"street"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shipping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#/$defs/address"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#/$defs/address"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For discriminated unions (event types, message kinds), &lt;code&gt;oneOf&lt;/code&gt; with a &lt;code&gt;const&lt;/code&gt; discriminator is the standard pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oneOf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"const"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"const"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sms"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;+[1-9]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;d{1,14}$"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A real signup schema
&lt;/h2&gt;

&lt;p&gt;Putting every keyword together, here is roughly the schema I now use for a signup endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json-schema.org/draft/2020-12/schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SignupRequest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;254&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^[a-zA-Z0-9_]{3,20}$"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AU"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"newsletter"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"referrals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"uniqueItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It enforces the email format with the 254-char maximum from RFC 5321, a 12-character minimum password from NIST SP 800-63B, a regex-validated username, an integer age within plausible bounds, a closed enum of supported countries, an optional boolean with a documented default, and an optional referral list capped at 5 unique emails. The trailing &lt;code&gt;additionalProperties: false&lt;/code&gt; is the line that would have saved me three days and 12,000 rows two years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling: Ajv (Node) and jsonschema (Python)
&lt;/h2&gt;

&lt;p&gt;Declare which draft you target with the &lt;code&gt;$schema&lt;/code&gt; keyword at the root. The two production-grade validators I reach for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ajv.js.org/" rel="noopener noreferrer"&gt;Ajv&lt;/a&gt; for Node.js and browser&lt;/strong&gt;, the fastest JS validator, supports Draft 2020-12. Install &lt;code&gt;ajv&lt;/code&gt; and &lt;code&gt;ajv-formats&lt;/code&gt; together if you use &lt;code&gt;format&lt;/code&gt;. Compile schemas once at startup with &lt;code&gt;const validate = ajv.compile(schema)&lt;/code&gt;, then call &lt;code&gt;validate(data)&lt;/code&gt; on every request. This is 10 to 100 times faster than recompiling per call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jsonschema&lt;/code&gt; for Python&lt;/strong&gt;, the reference Python validator. Use &lt;code&gt;Draft202012Validator(schema).validate(data)&lt;/code&gt; or iterate &lt;code&gt;.iter_errors(data)&lt;/code&gt; to surface all errors at once instead of failing on the first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For quick iteration without writing code, I usually paste the schema and a sample payload into a &lt;a href="https://calculators.im/json-formatter" rel="noopener noreferrer"&gt;JSON formatter&lt;/a&gt; to confirm both parse, then run them through a browser-based validator. When debugging an unexpected failure, a &lt;a href="https://calculators.im/diff-checker" rel="noopener noreferrer"&gt;diff checker&lt;/a&gt; helps me compare a failing payload against a known-good payload to spot the offending field.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON Schema vs OpenAPI vs TypeScript
&lt;/h2&gt;

&lt;p&gt;These three describe data shapes but solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript types&lt;/strong&gt; are compile-time only. They vanish at runtime, so a malformed API payload will silently corrupt your program if you trust the type without validating. Great for developer ergonomics, useless for runtime safety.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON Schema&lt;/strong&gt; is runtime validation that works in any language. Use it at API boundaries, for config files, for database documents, and for any cross-language data contract. A single schema can drive validation in your Node frontend, Python backend, and Go worker without rewriting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI&lt;/strong&gt; (formerly Swagger) wraps JSON Schema inside an API description. It adds endpoints, methods, status codes, authentication, examples, and tooling for client SDK generation. Use it when you are describing an HTTP API and want documentation, client codegen, and validation in one document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stack I default to now: write the JSON Schema as source of truth, generate TypeScript types from it with &lt;code&gt;json-schema-to-typescript&lt;/code&gt;, and embed the same schema inside an OpenAPI spec for HTTP routes. One source, three outputs, no drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistakes I kept making
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Forgetting &lt;code&gt;additionalProperties: false&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The original bug. Without it, any extra field passes validation. A client typo like &lt;code&gt;{ "emial": "x@y.com" }&lt;/code&gt; validates as "no email present plus an unknown field" instead of the clean error you want. Add it by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Confusing &lt;code&gt;required&lt;/code&gt; with &lt;code&gt;type&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Listing a property under &lt;code&gt;properties&lt;/code&gt; does NOT make it required. You must also add it to the &lt;code&gt;required&lt;/code&gt; array. Conversely, &lt;code&gt;required&lt;/code&gt; only checks presence. A wrong-type field still fails, but on the type check, not the required check.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Using &lt;code&gt;format&lt;/code&gt; without enabling assertion
&lt;/h3&gt;

&lt;p&gt;In Ajv you must &lt;code&gt;require('ajv-formats')(ajv)&lt;/code&gt;. In Python &lt;code&gt;jsonschema&lt;/code&gt; pass &lt;code&gt;format_checker=FormatChecker()&lt;/code&gt;. Without this, &lt;code&gt;format: email&lt;/code&gt; is metadata only and accepts any string. I burned half a day on this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;oneOf&lt;/code&gt; where &lt;code&gt;anyOf&lt;/code&gt; is correct
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;oneOf&lt;/code&gt; rejects data that matches more than one subschema. If your subschemas overlap (a value that is both a positive integer and a multiple of 5), &lt;code&gt;oneOf&lt;/code&gt; rejects. Use it only for genuinely disjoint cases like discriminated unions.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;code&gt;multipleOf&lt;/code&gt; with floats
&lt;/h3&gt;

&lt;p&gt;IEEE 754 cannot exactly represent 0.1. &lt;code&gt;{ "multipleOf": 0.1 }&lt;/code&gt; will reject values you expect to pass. Use integer units (cents, basis points) instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Recompiling schemas on every request
&lt;/h3&gt;

&lt;p&gt;Ajv's &lt;code&gt;compile()&lt;/code&gt; is expensive. The compiled validator is fast. Compile once at module load, store the function, reuse it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;JSON Schema looks verbose at first. Often the schema is longer than the data. That is the point. Every constraint you encode is one bug you cannot ship. Start with your top three API endpoints, then your config files, then your cross-service messages. Within a sprint you will catch at least one bug that would have made it to production.&lt;/p&gt;

&lt;p&gt;If you want a sandbox, try the &lt;a href="https://json-schema.org/learn" rel="noopener noreferrer"&gt;JSON Schema Reference Tutorial&lt;/a&gt; and an online validator like jsonschemavalidator.net. And if you ever debug a &lt;code&gt;pattern&lt;/code&gt; validation that is misbehaving, a &lt;a href="https://calculators.im/regex-tester" rel="noopener noreferrer"&gt;regex tester&lt;/a&gt; is faster than guessing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://calculators.im/blog/json-schema-10-minutes-validation-types-ajv-examples-guide" rel="noopener noreferrer"&gt;calculators.im&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>json</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Keep Forgetting Subnet Math — Here's the Cheat Sheet I Actually Use</title>
      <dc:creator>Anh Quân Nguyễn</dc:creator>
      <pubDate>Tue, 19 May 2026 03:17:29 +0000</pubDate>
      <link>https://forem.com/anh_qunnguyn_57549060f/i-keep-forgetting-subnet-math-heres-the-cheat-sheet-i-actually-use-37o7</link>
      <guid>https://forem.com/anh_qunnguyn_57549060f/i-keep-forgetting-subnet-math-heres-the-cheat-sheet-i-actually-use-37o7</guid>
      <description>&lt;p&gt;I have been writing backend code for years and I still cannot tell you, off the top of my head, how many usable hosts are in a &lt;code&gt;/22&lt;/code&gt;. Every time I open an AWS VPC config or a Kubernetes cluster blueprint, I do the same Google search: "subnet calculator", click whatever ranks first, type my CIDR, write down the answer. Then I forget all of it within a week.&lt;/p&gt;

&lt;p&gt;After the fourth or fifth time I had to look up whether &lt;code&gt;/27&lt;/code&gt; gives me 30 or 32 usable IPs, I sat down and built a one-page cheat sheet for myself. This post is that cheat sheet, plus the few mental shortcuts that have actually stuck. If you ship to cloud and only touch subnet math a few times a year, this is the post I wish I had bookmarked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three numbers that matter
&lt;/h2&gt;

&lt;p&gt;For any CIDR, only three numbers matter in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Block size&lt;/strong&gt; — how many IPs total. Formula: &lt;code&gt;2^(32 − prefix)&lt;/code&gt;. A &lt;code&gt;/24&lt;/code&gt; has 256 addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usable hosts&lt;/strong&gt; — block size minus 2 (one for the network address, one for the broadcast). A &lt;code&gt;/24&lt;/code&gt; has 254 usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Magic number&lt;/strong&gt; — &lt;code&gt;256 − mask_octet&lt;/code&gt;. Tells you where the next subnet starts. For &lt;code&gt;/26&lt;/code&gt; (mask &lt;code&gt;255.255.255.192&lt;/code&gt;), the magic number is &lt;code&gt;64&lt;/code&gt;, so subnets land at &lt;code&gt;.0&lt;/code&gt;, &lt;code&gt;.64&lt;/code&gt;, &lt;code&gt;.128&lt;/code&gt;, &lt;code&gt;.192&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Memorize one table and you will never need to do binary math in your head again:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Block size&lt;/th&gt;
&lt;th&gt;Usable hosts&lt;/th&gt;
&lt;th&gt;Mask&lt;/th&gt;
&lt;th&gt;Magic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/22&lt;/td&gt;
&lt;td&gt;1,024&lt;/td&gt;
&lt;td&gt;1,022&lt;/td&gt;
&lt;td&gt;255.255.252.0&lt;/td&gt;
&lt;td&gt;4 (3rd octet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/23&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;510&lt;/td&gt;
&lt;td&gt;255.255.254.0&lt;/td&gt;
&lt;td&gt;2 (3rd octet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/24&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;254&lt;/td&gt;
&lt;td&gt;255.255.255.0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/25&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;126&lt;/td&gt;
&lt;td&gt;255.255.255.128&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/26&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;255.255.255.192&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/27&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;255.255.255.224&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/28&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;255.255.255.240&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/29&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;255.255.255.248&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/30&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;255.255.255.252&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is the entire subnet math for 99% of cloud work. Print it, tape it to your monitor, move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The magic number trick, with a worked example
&lt;/h2&gt;

&lt;p&gt;Suppose someone gives you the IP &lt;code&gt;10.20.30.45/26&lt;/code&gt; and asks for the network and broadcast addresses. No calculator, no binary conversion.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prefix is &lt;code&gt;/26&lt;/code&gt;, so the magic number is &lt;code&gt;64&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Look at the last octet: &lt;code&gt;45&lt;/code&gt;. Which multiple of 64 does it fall into? &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;64&lt;/code&gt;, &lt;code&gt;128&lt;/code&gt;, &lt;code&gt;192&lt;/code&gt;. &lt;code&gt;45&lt;/code&gt; is in the &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;63&lt;/code&gt; block.&lt;/li&gt;
&lt;li&gt;Network address: &lt;code&gt;10.20.30.0&lt;/code&gt;. Broadcast: &lt;code&gt;10.20.30.63&lt;/code&gt; (one less than the next subnet, which would start at &lt;code&gt;.64&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Usable range: &lt;code&gt;10.20.30.1&lt;/code&gt; to &lt;code&gt;10.20.30.62&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same trick for prefixes that fall in the third octet. &lt;code&gt;10.20.30.45/22&lt;/code&gt; → magic number is &lt;code&gt;4&lt;/code&gt;, applied to the third octet. &lt;code&gt;30&lt;/code&gt; falls in the &lt;code&gt;28&lt;/code&gt;–&lt;code&gt;31&lt;/code&gt; block (because &lt;code&gt;28&lt;/code&gt; is the closest multiple of 4 at or below &lt;code&gt;30&lt;/code&gt;). Network: &lt;code&gt;10.20.28.0&lt;/code&gt;. Broadcast: &lt;code&gt;10.20.31.255&lt;/code&gt;. Block spans four &lt;code&gt;/24&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;If your subnet boundaries do not look "round" — say &lt;code&gt;10.20.30.0/22&lt;/code&gt; — that is a malformed CIDR. The network address must align to the magic number. &lt;code&gt;10.20.30.0&lt;/code&gt; is not a valid &lt;code&gt;/22&lt;/code&gt; start; &lt;code&gt;10.20.28.0/22&lt;/code&gt; is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud-specific gotchas you will hit
&lt;/h2&gt;

&lt;p&gt;The textbook subnet math is the easy part. What actually trips up cloud engineers is the platform-specific quirks layered on top.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS VPC
&lt;/h3&gt;

&lt;p&gt;AWS reserves &lt;strong&gt;five&lt;/strong&gt; addresses per subnet, not two. The network address, the broadcast, and three more: the VPC router, DNS, and a future-use address. So a &lt;code&gt;/28&lt;/code&gt; AWS subnet has 11 usable IPs, not 14. This catches people every time they try to fit "exactly 14 EC2 instances" into a &lt;code&gt;/28&lt;/code&gt; and run out of IPs at instance #12.&lt;/p&gt;

&lt;p&gt;VPC CIDR sizing also has hard limits: minimum &lt;code&gt;/28&lt;/code&gt;, maximum &lt;code&gt;/16&lt;/code&gt;. You can attach secondary CIDR blocks, but you cannot ever shrink a primary CIDR after creation. &lt;strong&gt;Always size up.&lt;/strong&gt; A &lt;code&gt;/16&lt;/code&gt; gives you 65,536 addresses for free; there is no cost penalty for picking a large VPC CIDR and no benefit to picking a tight one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes
&lt;/h3&gt;

&lt;p&gt;A typical EKS or GKE cluster needs three separate CIDRs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node CIDR&lt;/strong&gt; — a subnet of the VPC, one IP per node (plus AWS's five). Size for max-nodes × 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pod CIDR&lt;/strong&gt; — usually &lt;code&gt;10.244.0.0/16&lt;/code&gt; (Flannel default) or &lt;code&gt;192.168.0.0/16&lt;/code&gt; (Calico). Each node gets a &lt;code&gt;/24&lt;/code&gt; slice (Flannel) so the cluster maxes out at 254 nodes with a &lt;code&gt;/16&lt;/code&gt; pod CIDR. Larger clusters need &lt;code&gt;/12&lt;/code&gt; or smaller per-node allocations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service CIDR&lt;/strong&gt; — usually &lt;code&gt;10.96.0.0/12&lt;/code&gt; (kubeadm default). Sized for total Services, not Pods. A &lt;code&gt;/16&lt;/code&gt; is overkill for most clusters; a &lt;code&gt;/20&lt;/code&gt; is plenty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most common k8s networking mistake is overlapping the pod CIDR with the VPC CIDR. Pick non-overlapping ranges from day one — renumbering pod CIDR in a running cluster is a recreate-the-cluster operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker
&lt;/h3&gt;

&lt;p&gt;Docker's default bridge is &lt;code&gt;172.17.0.0/16&lt;/code&gt;. The default &lt;code&gt;docker network create&lt;/code&gt; allocates from &lt;code&gt;172.18.0.0/16&lt;/code&gt; upward in &lt;code&gt;/24&lt;/code&gt; chunks. If your corporate VPN also uses &lt;code&gt;172.x.x.x&lt;/code&gt; ranges (very common), you will get phantom routing failures where Docker containers can reach the internet but cannot reach internal services. Reconfigure Docker's default address pools in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default-address-pools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.99.0.0/16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This carves Docker out of the &lt;code&gt;172.x&lt;/code&gt; space and into a private &lt;code&gt;10.x&lt;/code&gt; range your VPN almost certainly does not use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subnet math in code (Python and Go)
&lt;/h2&gt;

&lt;p&gt;When you actually need to compute subnets programmatically — generating Terraform, validating user input, planning a migration — every modern language has a stdlib for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt; uses &lt;code&gt;ipaddress&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;

&lt;span class="c1"&gt;# Parse a network
&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.20.30.0/22&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;network_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# 10.20.28.0 — auto-normalized!
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broadcast_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 10.20.31.255
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_addresses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# 1024
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt; &lt;span class="c1"&gt;# 1022 — excludes network + broadcast
&lt;/span&gt;
&lt;span class="c1"&gt;# Check if an IP is in a subnet
&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.20.29.50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# True
&lt;/span&gt;
&lt;span class="c1"&gt;# Subdivide a /22 into /24s
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subnets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 10.20.28.0/24
# 10.20.29.0/24
# 10.20.30.0/24
# 10.20.31.0/24
&lt;/span&gt;
&lt;span class="c1"&gt;# Detect overlap (catches the VLSM mistake from earlier)
&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.0/25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.64/26&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;overlaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: passing &lt;code&gt;strict=False&lt;/code&gt; to &lt;code&gt;ip_network&lt;/code&gt; lets you parse &lt;code&gt;10.20.30.0/22&lt;/code&gt; (a malformed CIDR — &lt;code&gt;.30&lt;/code&gt; is not the network address). With &lt;code&gt;strict=True&lt;/code&gt; (the default since Python 3.9) it raises &lt;code&gt;ValueError&lt;/code&gt;. Use &lt;code&gt;strict=True&lt;/code&gt; in production input validation; the explicit error is far more useful than a silently-corrected network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go&lt;/strong&gt; uses &lt;code&gt;net/netip&lt;/code&gt; (Go 1.18+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"net/netip"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;netip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParsePrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"10.20.30.0/22"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// Normalize to actual network address&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Masked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c"&gt;// 10.20.28.0&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bits&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c"&gt;// 22&lt;/span&gt;

    &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;netip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseAddr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"10.20.29.50"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c"&gt;// true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The older &lt;code&gt;net.IPNet&lt;/code&gt; API still works but &lt;code&gt;net/netip&lt;/code&gt; is value-typed, allocation-free, and the recommended choice for new code.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 30-second AWS CLI sanity check
&lt;/h2&gt;

&lt;p&gt;When debugging "why can't my EC2 instance reach this other EC2 instance," step one is always: are they on subnets that route to each other? Quick CLI check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List subnets with their CIDRs in the current VPC&lt;/span&gt;
aws ec2 describe-subnets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Subnets[*].[SubnetId,CidrBlock,AvailabilityZone]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Check route table for a subnet&lt;/span&gt;
aws ec2 describe-route-tables &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;association.subnet-id,Values&lt;span class="o"&gt;=&lt;/span&gt;subnet-abc123 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'RouteTables[*].Routes'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If two subnets are in the same VPC and the route tables both have &lt;code&gt;local&lt;/code&gt; routes for the VPC CIDR, they can reach each other (assuming security groups and NACLs allow). Most "why no connectivity" tickets resolve here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three mistakes I have personally shipped to production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Subnet too small.&lt;/strong&gt; A &lt;code&gt;/28&lt;/code&gt; for "we only need 10 instances" — then AWS takes 5, leaving 11. Add a load balancer, add scaling, you are out of IPs. Always start at &lt;code&gt;/24&lt;/code&gt; for any production subnet unless you have a hard reason not to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pod CIDR overlapping VPC CIDR.&lt;/strong&gt; Created a GKE cluster with default pod CIDR &lt;code&gt;10.0.0.0/14&lt;/code&gt; inside a VPC at &lt;code&gt;10.0.0.0/16&lt;/code&gt;. Cluster came up, pods on the same node could reach each other, pods on different nodes silently dropped traffic. Three hours debugging later: pick non-overlapping CIDRs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgot AWS's 5 reserved IPs.&lt;/strong&gt; Provisioned 14 EC2 instances in a fresh &lt;code&gt;/28&lt;/code&gt;. First 11 launched fine, the rest failed with &lt;code&gt;InsufficientFreeAddressesInSubnet&lt;/code&gt;. The fix is &lt;code&gt;/27&lt;/code&gt; (32 addresses, 27 usable after AWS reservations).&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing — make subnet math boring
&lt;/h2&gt;

&lt;p&gt;The whole point of the cheat sheet is that subnet math should be boring. You should not have to think about it. You should be able to glance at a CIDR, know the block size and the magic number, and move on to whatever interesting problem you were actually trying to solve.&lt;/p&gt;

&lt;p&gt;If you need to verify a non-trivial CIDR or plan a VLSM allocation across multiple subnets, I built a free &lt;a href="https://calculators.im/subnet-calculator" rel="noopener noreferrer"&gt;subnet calculator&lt;/a&gt; that handles IPv4, IPv6, and VLSM planning. There's also a &lt;a href="https://calculators.im/blog/subnet-calculator-guide-cidr-vlsm-ipv4-ipv6-network-math" rel="noopener noreferrer"&gt;longer-form guide on the math behind CIDR, VLSM, and IPv6 subnetting&lt;/a&gt; if you want to go deeper than this cheat sheet.&lt;/p&gt;

&lt;p&gt;The tool is free, no signup, no tracking. If it saves you a Google search next quarter, that is the entire goal.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Cross-posted with canonical from &lt;a href="https://calculators.im/blog/subnet-calculator-guide-cidr-vlsm-ipv4-ipv6-network-math" rel="noopener noreferrer"&gt;calculators.im&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>devops</category>
      <category>aws</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Lessons Learned Building 270+ Online Calculators: A Developer's Deep Dive</title>
      <dc:creator>Anh Quân Nguyễn</dc:creator>
      <pubDate>Tue, 21 Apr 2026 10:26:03 +0000</pubDate>
      <link>https://forem.com/anh_qunnguyn_57549060f/lessons-learned-building-270-online-calculators-a-developers-deep-dive-3nhf</link>
      <guid>https://forem.com/anh_qunnguyn_57549060f/lessons-learned-building-270-online-calculators-a-developers-deep-dive-3nhf</guid>
      <description>&lt;p&gt;When I started building &lt;a href="https://calculators.im" rel="noopener noreferrer"&gt;calculators.im&lt;/a&gt;, I thought it would be a simple project — create a few calculator pages, deploy, done. Two years and 270+ calculators later, I've learned more about web performance, SEO, and product scaling than I ever expected. Here's everything I wish someone had told me before I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project: What Is calculators.im?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://calculators.im" rel="noopener noreferrer"&gt;calculators.im&lt;/a&gt; is a collection of 270+ free online calculators covering everything from finance (mortgage, compound interest, ROI) to health (BMI, calorie needs, pregnancy due date) to math (scientific, percentage, fractions) and beyond.&lt;/p&gt;

&lt;p&gt;The tech stack: &lt;strong&gt;Go&lt;/strong&gt; for the backend, &lt;strong&gt;HTMX&lt;/strong&gt; for interactivity, &lt;strong&gt;Tailwind CSS&lt;/strong&gt; for styling, &lt;strong&gt;Redis&lt;/strong&gt; for caching, and &lt;strong&gt;NGINX + Cloudflare&lt;/strong&gt; for serving.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 1: SEO Is 80% of Your Traffic — Treat It as a Feature
&lt;/h2&gt;

&lt;p&gt;I originally thought "build good calculators and they'll come." Wrong.&lt;/p&gt;

&lt;p&gt;The reality: calculator sites are brutally competitive. There are dozens of sites competing for every keyword like "mortgage calculator" or "BMI calculator." Here's what actually works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target long-tail keywords over short ones.&lt;/strong&gt; Instead of competing for "loan calculator" (dominated by Bankrate, NerdWallet), target "car loan calculator with extra payments" or "loan calculator with balloon payment." Lower competition, still significant volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema markup is non-negotiable.&lt;/strong&gt; Adding &lt;code&gt;WebApplication&lt;/code&gt; and &lt;code&gt;FAQPage&lt;/code&gt; schema to each calculator dramatically increased click-through rates. Google often shows rich results in SERPs for tool pages with proper schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page speed = ranking factor for tools.&lt;/strong&gt; Users expect calculators to respond instantly. Every 100ms of latency costs you. With Go + HTMX, our Time to First Byte (TTFB) is under 50ms globally thanks to Cloudflare caching.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 2: HTMX Was the Right Bet — But Has Trade-offs
&lt;/h2&gt;

&lt;p&gt;Choosing HTMX over React was controversial when I started. Here's the honest verdict after building 270 pages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero client-side hydration = better Core Web Vitals (LCP, CLS)&lt;/li&gt;
&lt;li&gt;SEO-friendly by default — all content is server-rendered&lt;/li&gt;
&lt;li&gt;Bundle size: ~14kb for htmx vs 130kb+ for a React app&lt;/li&gt;
&lt;li&gt;Simpler mental model: HTML attributes over JavaScript state management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex interactions (real-time charts, drag-and-drop) require either Hyperscript or vanilla JS alongside HTMX&lt;/li&gt;
&lt;li&gt;The community is smaller than React's, so fewer StackOverflow answers&lt;/li&gt;
&lt;li&gt;Some HTMX patterns feel verbose for highly dynamic UIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Would I choose HTMX again?&lt;/strong&gt; Yes — especially for content-heavy tool sites where SEO matters. For a complex SaaS dashboard, I'd reconsider.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 3: Go Is Overkill for Small Projects — Perfect for Scale
&lt;/h2&gt;

&lt;p&gt;Go was genuinely the right choice, but not for the reasons I expected:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory footprint:&lt;/strong&gt; Our entire server runs on a $6/month VPS and handles thousands of daily visits with ~30MB RAM usage. A Node.js equivalent would use 5-10x more memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compilation catches bugs early.&lt;/strong&gt; Go's strict type system eliminated an entire category of runtime errors that plague dynamic-language projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single binary deployment.&lt;/strong&gt; &lt;code&gt;go build&lt;/code&gt;, rsync to server, restart. No npm install, no dependency conflicts, no environment issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The unexpected lesson:&lt;/strong&gt; Go forces you to be explicit about error handling. At first this feels tedious (&lt;code&gt;if err != nil&lt;/code&gt; everywhere), but it makes your code dramatically more reliable in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 4: Calculator UX Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;Building a "simple" calculator that delights users involves surprisingly subtle decisions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input formatting:&lt;/strong&gt; Users expect numbers to auto-format (1000 → 1,000) as they type. Implementing this with HTMX + Go required careful input sanitization to avoid formatting conflicts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instant results:&lt;/strong&gt; Never make users click a "Calculate" button. Use HTMX's &lt;code&gt;hx-trigger="input"&lt;/code&gt; to recalculate on every keystroke. Users expect calculator-app behavior from web calculators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile-first number inputs:&lt;/strong&gt; On mobile, use &lt;code&gt;inputmode="decimal"&lt;/code&gt; rather than &lt;code&gt;type="number"&lt;/code&gt;. The decimal keyboard is much better than the native number input's spinner interface for financial inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error states matter:&lt;/strong&gt; When a user enters invalid input (letters in a number field, negative age, etc.), show clear, friendly error messages in context — not a page-level alert.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 5: The Content Around the Calculator Matters More Than the Calculator
&lt;/h2&gt;

&lt;p&gt;This was my biggest revelation: the page content surrounding the calculator is more important for SEO than the calculator itself.&lt;/p&gt;

&lt;p&gt;Every calculator page on &lt;a href="https://calculators.im" rel="noopener noreferrer"&gt;calculators.im&lt;/a&gt; includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 500-800 word explanation of the formula and methodology&lt;/li&gt;
&lt;li&gt;A worked example with real numbers&lt;/li&gt;
&lt;li&gt;An FAQ section targeting "People Also Ask" queries&lt;/li&gt;
&lt;li&gt;A table of common values for reference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This content strategy is what earns backlinks, ranks for informational queries, and keeps users on the page longer — all positive signals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 6: Redis Caching Saved Us at Scale
&lt;/h2&gt;

&lt;p&gt;When a Reddit post linked to our &lt;a href="https://calculators.im/percentage-calculator" rel="noopener noreferrer"&gt;percentage calculator&lt;/a&gt;, we got ~8,000 visits in 2 hours. Without Redis caching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every request would hit Go and render the page fresh&lt;/li&gt;
&lt;li&gt;At 8,000 req/hr, that's manageable but wasteful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Redis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static page HTML is cached for 1 hour&lt;/li&gt;
&lt;li&gt;Dynamic calculation results are cached by input parameters&lt;/li&gt;
&lt;li&gt;Cache hit rate during the spike: 94%&lt;/li&gt;
&lt;li&gt;P99 response time during the spike: 18ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The caching strategy: cache at two layers — page-level (full HTML response via Cloudflare) and calculation-level (Redis for computed results with the same inputs).&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 7: Build the Infrastructure Once, Deploy 270 Times
&lt;/h2&gt;

&lt;p&gt;The most important architectural decision was treating calculators as data, not code.&lt;/p&gt;

&lt;p&gt;Each calculator is defined by a YAML config file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input fields with types, labels, validation rules&lt;/li&gt;
&lt;li&gt;Formula as a Go expression&lt;/li&gt;
&lt;li&gt;Output formatting&lt;/li&gt;
&lt;li&gt;SEO metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Go backend reads these configs and generates both the HTML and the calculation logic dynamically. Adding a new calculator takes about 15 minutes — write the YAML, write the formula, deploy.&lt;/p&gt;

&lt;p&gt;This approach let us go from 10 calculators to 270+ without a linear increase in development time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with the content strategy, not the calculator.&lt;/strong&gt; Write the explanatory content first, then build the tool around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add structured data from day 1.&lt;/strong&gt; Retrofitting schema markup to 270 pages was painful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build analytics from the start.&lt;/strong&gt; Understanding which calculators get used heavily vs. which ones are ignored shapes your roadmap.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Stack in Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Go — fast, cheap to run, rock-solid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend interactivity:&lt;/strong&gt; HTMX — SEO-friendly, tiny footprint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS — consistent, maintainable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching:&lt;/strong&gt; Redis — essential at any meaningful scale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; NGINX + Cloudflare — global performance, free DDoS protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building a similar tool site, I hope this saves you some of the trial and error. Check out &lt;a href="https://calculators.im" rel="noopener noreferrer"&gt;calculators.im&lt;/a&gt; to see what 270 calculators looks like in practice.&lt;/p&gt;

&lt;p&gt;What questions do you have? Drop them in the comments — happy to go deeper on any of these lessons.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>go</category>
      <category>seo</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
