<?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>DEV Community: Skojio Community</title>
    <description>The latest articles on DEV Community by Skojio Community (@skojiocommunity).</description>
    <link>https://dev.to/skojiocommunity</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3942038%2F8cc647c1-bcdb-490e-8433-06e8130e2c50.png</url>
      <title>DEV Community: Skojio Community</title>
      <link>https://dev.to/skojiocommunity</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/skojiocommunity"/>
    <language>en</language>
    <item>
      <title>Verifying a JWT Signature: HS256 vs RS256 (and What exp Actually Means)</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Sat, 11 Jul 2026 10:05:03 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/verifying-a-jwt-signature-hs256-vs-rs256-and-what-exp-actually-means-3915</link>
      <guid>https://dev.to/skojiocommunity/verifying-a-jwt-signature-hs256-vs-rs256-and-what-exp-actually-means-3915</guid>
      <description>&lt;p&gt;If you've ever pasted a JWT into a debugger just to see three chunks of jumbled text and a green tick you didn't fully trust, this is for you. Decoding a JWT is the easy 90% of the job — reading the header and payload back as JSON takes one line of code. The part that actually matters for security — verifying the signature — is where most explanations either wave their hands or skip straight to "just use a library." This walks through what a JWT's three segments actually contain, how &lt;code&gt;exp&lt;/code&gt; and the other standard claims work, and what it genuinely means to verify a signature for the two families you'll meet most often: HMAC (HS256) and public-key (RS256/ES256).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a JWT actually is: three segments, not one blob
&lt;/h2&gt;

&lt;p&gt;A JSON Web Token is three base64url-encoded pieces joined by dots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;header.payload.signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;header&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;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payload&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;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1751500000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1751503600&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;Base64url is not encryption — it's a reversible text encoding, the same way a QR code isn't a secret. Anyone holding a JWT can decode the header and payload themselves with nothing more than &lt;code&gt;atob()&lt;/code&gt; and a JSON parser. This surprises people the first time they realise it: &lt;strong&gt;a JWT's contents are never confidential&lt;/strong&gt;, only (potentially) tamper-evident. If you need to hide the payload's contents from the bearer of the token, a JWT is the wrong tool — that's what encrypted JWTs (JWE) or an opaque, server-side session token are for.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Never put anything you don't want the token holder to read in a JWT's payload — session IDs, role names, and user IDs are fine; raw passwords, unmasked card numbers, or anything you'd wince at appearing in a browser's devtools are not.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the standard claims: what exp, iat, nbf, aud, iss actually mean
&lt;/h2&gt;

&lt;p&gt;The payload can hold whatever your application needs, but RFC 7519 defines a handful of registered claims most real-world tokens use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Expiry — Unix timestamp after which the token must be rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Issued-at — Unix timestamp when the token was created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nbf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not-before — Unix timestamp before which the token must NOT be accepted yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Audience — who the token is intended for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Issuer — who created the token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subject — usually the user or account the token represents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jti&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unique token ID, often used to detect replay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three time-based claims (&lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt;, &lt;code&gt;nbf&lt;/code&gt;) are all plain Unix seconds — no timezone, no ISO string, just an integer. That's exactly why "is my token expired?" is a more annoying question to answer by eye than it should be: you have to convert the number to a date yourself, and get the units right (seconds, not milliseconds — a common off-by-1000 bug). A &lt;code&gt;1751503600&lt;/code&gt; sitting in a payload tells you nothing at a glance; converted, it's a specific date and time you can actually reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decode first, verify second — and why the order matters
&lt;/h2&gt;

&lt;p&gt;Given a token, there are two genuinely different questions you can ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"What does this token claim?"&lt;/strong&gt; — answered by decoding. Split on the two dots, base64url-decode the header and payload, done. No key required, no trust implied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Should I believe what it claims?"&lt;/strong&gt; — answered by verifying. This requires the correct key and tells you whether the header and payload have been altered since they were signed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A worryingly common mistake is reading claims out of a decoded-but-unverified token and acting on them — trusting a &lt;code&gt;role: admin&lt;/code&gt; claim, say, without ever checking the signature. If the payload is just base64-decoded JSON, it's exactly as trustworthy as a text file the user handed you: anyone can write &lt;code&gt;{"role":"admin"}&lt;/code&gt; into it. Decoding without verifying tells you what a token &lt;em&gt;says&lt;/em&gt;; only verification tells you whether it's &lt;em&gt;true&lt;/em&gt;.&lt;/p&gt;



&lt;h2&gt;
  
  
  Verifying HS256: the shared-secret family
&lt;/h2&gt;

&lt;p&gt;HS256 (and its longer siblings HS384, HS512) use HMAC — a single shared secret that both the issuer and the verifier must hold. Verification recomputes the HMAC over the token's first two segments (&lt;code&gt;base64url(header) + "." + base64url(payload)&lt;/code&gt;) using that secret, and compares the result byte-for-byte against the signature segment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expected_signature = HMAC-SHA256(secret, header + "." + payload)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;expected_signature&lt;/code&gt; matches the token's actual signature segment, the token is genuine and untampered — provided the secret itself was never leaked. That's HS256's whole trust model in one sentence: &lt;strong&gt;anyone who holds the secret can both sign new tokens and verify existing ones&lt;/strong&gt;, which is fine for a single backend service checking its own tokens, and a liability the moment that secret needs to be shared across multiple services or handed to a third party.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying RS256 and ES256: the public/private-key family
&lt;/h2&gt;

&lt;p&gt;RS256 (RSA) and ES256 (ECDSA) split signing and verifying into two different keys. A &lt;strong&gt;private key&lt;/strong&gt; signs; a completely separate, non-secret &lt;strong&gt;public key&lt;/strong&gt; verifies. This solves HS256's sharing problem cleanly: an identity provider can publish its public key openly, and any number of services can verify tokens against it without ever holding anything sensitive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signature = Sign(private_key, header + "." + payload)     // issuer only
is_valid  = Verify(public_key, header + "." + payload, signature)  // any verifier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RS256 uses RSA keys (typically PEM or JWK format); ES256 uses elliptic-curve keys, which are considerably smaller for equivalent strength — a practical reason ES256 has become popular where token size matters (URLs, headers, mobile). Either way, the shape of the trust model is identical: &lt;strong&gt;verifying a token never requires the private key&lt;/strong&gt;, only the issuer's public key, which by design is safe to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two JWT security traps worth knowing by name
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;alg: none&lt;/code&gt;.&lt;/strong&gt; The JWT spec allows a header to declare &lt;code&gt;alg: none&lt;/code&gt;, meaning the token is deliberately unsigned — legitimate for some internal testing scenarios, but catastrophic if a verifier ever accepts one from the outside world without an explicit, deliberate opt-in. Some older or misconfigured libraries did exactly that: they read &lt;code&gt;alg&lt;/code&gt; from the token itself and, on seeing &lt;code&gt;none&lt;/code&gt;, skipped verification entirely — letting an attacker forge any payload they liked with zero key material. Any tool or library you use to inspect JWTs should flag &lt;code&gt;alg: none&lt;/code&gt; loudly rather than quietly decode it as if nothing were wrong.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
If you ever see alg: none on a token you didn't generate yourself for testing, treat it as a red flag, not a curiosity — it means the token carries no proof of integrity at all.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RS256-to-HS256 key confusion.&lt;/strong&gt; This one is subtler. If a verifier trusts the &lt;code&gt;alg&lt;/code&gt; value written in the token itself, an attacker can take a service's own — publicly available — RSA public key and resign a token with HS256, using that public key's bytes as the HMAC secret. A verifier that blindly does "whatever &lt;code&gt;alg&lt;/code&gt; says, use this key" can end up HMAC-verifying against a key it never intended to be a shared secret, and pass. The fix lives entirely on the verifying side: &lt;strong&gt;pin the algorithm you expect&lt;/strong&gt; (e.g. "this service only ever accepts RS256") rather than trusting the token to tell you what algorithm to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it: decode, verify, and build test tokens in one page
&lt;/h2&gt;

&lt;p&gt;Skojio's &lt;a href="https://dev.to/tools/jwt-decoder"&gt;JWT Decoder, Verifier &amp;amp; Encoder&lt;/a&gt; covers the whole loop described above in one page, entirely in your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paste a token to see its header and payload decoded and every standard claim humanised — including &lt;code&gt;exp&lt;/code&gt;/&lt;code&gt;iat&lt;/code&gt;/&lt;code&gt;nbf&lt;/code&gt; converted to local date/time, not just a raw integer&lt;/li&gt;
&lt;li&gt;An at-a-glance validity banner tells you expired / not-yet-valid / valid, with a relative time ("expired 3 days ago")&lt;/li&gt;
&lt;li&gt;Verify HS256/384/512 against a shared secret, or RS256/384/512 and ES256/384/512 against a public key (PEM or JWK) — all via the browser's native WebCrypto, nothing sent anywhere&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;alg: none&lt;/code&gt; tokens get a non-dismissible warning instead of a silent pass, and pasting a public key against an HMAC-alg token triggers an explicit key-confusion warning&lt;/li&gt;
&lt;li&gt;An Encode &amp;amp; Sign mode lets you build and sign a test token from scratch — handy for reproducing an expiry bug or a malformed-token scenario without reaching for a CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It doesn't claim to be more private than every other JWT tool out there — several others are also client-side. What it does offer is a complete, vendor-neutral page with no account, no marketing chrome, and both directions (decode/verify and encode/sign) covered for all nine mainstream algorithms in one place. If you need a shared secret for HS256 testing, &lt;a href="https://dev.to/tools/password-generator"&gt;Skojio's Password Generator&lt;/a&gt; will generate one; if you're inspecting the base64url encoding itself rather than a full token, &lt;a href="https://dev.to/tools/base64"&gt;Base64 Encoder/Decoder&lt;/a&gt; is the standalone version of that same step.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;A JWT's header and payload are base64url-encoded, not encrypted — anyone holding the token can read them, so never put secrets in the payload&lt;/li&gt;
&lt;li&gt;Decoding tells you what a token claims; only verifying its signature tells you whether to believe those claims&lt;/li&gt;
&lt;li&gt;HS256 uses one shared secret for both signing and verifying; RS256/ES256 split signing (private key) from verifying (public key), which scales better across multiple services&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt;, and &lt;code&gt;nbf&lt;/code&gt; are plain Unix-second timestamps — convert them to check expiry rather than eyeballing the raw integer&lt;/li&gt;
&lt;li&gt;Watch for two specific traps: &lt;code&gt;alg: none&lt;/code&gt; tokens that carry no signature at all, and RS256-to-HS256 key confusion, where a verifier that trusts the token's own &lt;code&gt;alg&lt;/code&gt; field can be fooled into HMAC-checking against a public key
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>jwt</category>
      <category>developertools</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Funny Passphrases: Easy to Remember, Hard to Guess</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:05:36 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/funny-passphrases-easy-to-remember-hard-to-guess-51m</link>
      <guid>https://dev.to/skojiocommunity/funny-passphrases-easy-to-remember-hard-to-guess-51m</guid>
      <description>&lt;p&gt;Most password advice boils down to "use more characters and more symbols" — sound advice, and also&lt;br&gt;
the reason so many people end up with a password they can't actually remember and have to reset every&lt;br&gt;
few weeks. A &lt;strong&gt;passphrase&lt;/strong&gt; takes a different route to the same strength: instead of packing&lt;br&gt;
randomness into a short string of mixed characters, it spreads that randomness across several whole&lt;br&gt;
words. The result is longer to type but, done properly, considerably easier to recall.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Length beats complexity once you're past a basic threshold of character variety — a longer phrase
of ordinary words can out-resist a short, symbol-heavy password.&lt;/li&gt;
&lt;li&gt;A classic &lt;strong&gt;diceware generator&lt;/strong&gt; draws each word independently from a huge, unrestricted wordlist —
the "correct horse battery staple" approach.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;funny-sentence&lt;/strong&gt; passphrase trades a little entropy per word for a vivid, memorable mini-story —
useful, but only if the generator is honest about the trade-off.&lt;/li&gt;
&lt;li&gt;Whichever style you use, generate it somewhere that runs entirely in your browser and never sends
the result anywhere.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Why a random passphrase generator beats a character-soup password
&lt;/h2&gt;

&lt;p&gt;A brute-force attacker has to search through every possible combination until they hit yours. What&lt;br&gt;
matters for that search space is entropy — measured in bits, it's the number of times the number of&lt;br&gt;
guesses doubles before your secret is found on average. Two things drive entropy up: how many&lt;br&gt;
possible symbols could appear at each position, and how many positions there are.&lt;/p&gt;

&lt;p&gt;Character-random passwords lean on the first lever: mixing uppercase, lowercase, digits, and symbols&lt;br&gt;
widens the pool of possible characters at each slot. Passphrases lean on the second: instead of one&lt;br&gt;
character per slot, each "slot" is an entire word drawn from a wordlist of thousands. Six words drawn&lt;br&gt;
independently from a 7,776-word list carry roughly 77–78 bits of entropy — solidly in the "strong"&lt;br&gt;
band — while being nothing more exotic to type than six ordinary words in a row.&lt;/p&gt;


&lt;h2&gt;
  
  
  "Correct horse battery staple": the diceware idea, explained
&lt;/h2&gt;

&lt;p&gt;The phrase "correct horse battery staple" comes from a widely shared webcomic making exactly this&lt;br&gt;
point: a short, symbol-heavy password can be weaker than a few unrelated words strung together,&lt;br&gt;
despite looking scarier to type. The technique behind it — &lt;strong&gt;diceware&lt;/strong&gt; — is simple and auditable:&lt;br&gt;
roll dice (or, online, use a cryptographically secure random number generator) to pick words&lt;br&gt;
independently from a fixed, public wordlist, then join them together. The EFF's long wordlist&lt;br&gt;
(7,776 words, public domain) is the standard choice, and it's exactly what a proper &lt;strong&gt;diceware&lt;br&gt;
generator&lt;/strong&gt; should be drawing from — not a smaller, "curated for vibes" list that quietly reduces&lt;br&gt;
your entropy without telling you.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
The strength of a diceware passphrase is easy to check yourself: multiply your word count by&lt;br&gt;
log2(wordlist size). Six words from the 7,776-word EFF list gives roughly 6 × 12.92 ≈&lt;br&gt;
77.5 bits — no black box required.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;The trade-off with plain diceware is memorability. Four or six unrelated nouns don't naturally link&lt;br&gt;
together in your head, so you're still rote-memorising an arbitrary sequence — just a sequence of&lt;br&gt;
words instead of characters.&lt;/p&gt;
&lt;h2&gt;
  
  
  When funny beats plain: the memorability trick
&lt;/h2&gt;

&lt;p&gt;This is where a &lt;strong&gt;funny password generator&lt;/strong&gt; approach earns its place, provided it's honest about&lt;br&gt;
what it's doing. Instead of drawing unrelated words, a grammar-template generator arranges words into&lt;br&gt;
a fixed slot order — adjective, noun, verb, and so on — so the output reads as a tiny, absurd&lt;br&gt;
sentence rather than a word list. "angry-teapot-juggles-purple-donkeys" forms a mental picture in a&lt;br&gt;
way that four random nouns never quite manage, which genuinely helps recall.&lt;/p&gt;

&lt;p&gt;The honest catch: each slot in a grammar template is drawn from a smaller, purpose-built list (the&lt;br&gt;
adjective list, the noun list, the verb list) rather than one enormous shared wordlist, so the&lt;br&gt;
entropy per word is usually lower than free-form diceware. A tool that quietly skips over this either&lt;br&gt;
doesn't understand its own maths or doesn't want you to notice. Skojio's Passphrase Generator shows&lt;br&gt;
the exact bit count for whichever mode you're in, plus a standing note whenever the funny-sentence&lt;br&gt;
mode is active, reminding you to switch to Classic Diceware, use the longer template, or add extra&lt;br&gt;
digits and symbols if you need maximum resistance.&lt;/p&gt;
&lt;h2&gt;
  
  
  Numbers, symbols, and separators — do they matter?
&lt;/h2&gt;

&lt;p&gt;Adding one or two random digits, or a single symbol, contributes a small, disclosed amount of extra&lt;br&gt;
entropy on top of your word-based bits. It's a genuine bonus, but it isn't a substitute for enough&lt;br&gt;
words — three digits add roughly 10 bits, while a single extra six-letter word typically adds far&lt;br&gt;
more. Separators (hyphens, periods, spaces) don't affect entropy at all; pick whichever is easiest to&lt;br&gt;
type on the keyboard or device you'll be using the passphrase with, and avoid a letter-or-digit&lt;br&gt;
separator that could visually blend into the words either side of it. If a site specifically demands&lt;br&gt;
digits and symbols, a passphrase generator with numbers and symbols support (rather than one that&lt;br&gt;
only ever outputs bare words) lets you satisfy that policy without giving up the memorability.&lt;/p&gt;
&lt;h2&gt;
  
  
  Password vs passphrase: which should you use?
&lt;/h2&gt;

&lt;p&gt;Neither replaces the other — they suit different situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passphrase&lt;/strong&gt;: master password for a password manager, Wi-Fi network password, device unlock code,
or a recovery phrase you need to recall without writing it down. Anywhere you'll actually type or
remember the secret yourself, length-based passphrases are usually the easier win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password&lt;/strong&gt;: an individual site login you'll only ever paste from a password manager. Here, a
short, maximum-entropy, fully random string generated by Skojio's
&lt;a href="https://dev.to/tools/password-generator"&gt;Password Generator&lt;/a&gt; is the more familiar, equally strong fit — you
never have to type or remember it at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Skojio's &lt;a href="https://dev.to/tools/passphrase-generator"&gt;Passphrase Generator&lt;/a&gt; runs both approaches side by side: a&lt;br&gt;
Funny Sentence mode for something memorable, and a Classic Diceware mode using the full EFF long&lt;br&gt;
wordlist, with one transparent, mode-aware entropy meter for whichever you pick. Everything is&lt;br&gt;
generated using the browser's own cryptographically secure random number generator — nothing you&lt;br&gt;
generate is ever sent to a server, stored, or logged.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Length beats complexity — a passphrase of ordinary words can out-resist a short, symbol-heavy&lt;br&gt;
password, and it's far easier to actually remember.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;If you'd rather keep the character-random style for logins you'll always paste from a manager,&lt;br&gt;
Skojio's &lt;a href="https://dev.to/tools/password-generator"&gt;Password Generator&lt;/a&gt; covers that case with the same bulk&lt;br&gt;
generation, export, and honest strength metre.&lt;/p&gt;

</description>
      <category>security</category>
      <category>passwords</category>
      <category>passphrases</category>
    </item>
    <item>
      <title>How to Convert SOAP XML to JSON (and Back) Without Losing Attributes or Namespaces</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:05:05 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/how-to-convert-soap-xml-to-json-and-back-without-losing-attributes-or-namespaces-fol</link>
      <guid>https://dev.to/skojiocommunity/how-to-convert-soap-xml-to-json-and-back-without-losing-attributes-or-namespaces-fol</guid>
      <description>&lt;p&gt;Anyone who has had to debug a SOAP integration knows the moment: you're staring at a namespaced, attribute-heavy XML envelope, you need to inspect or mock it as JSON for a script or test fixture, and the free converter you paste it into either drops the attributes, strips the namespace prefixes, or turns a single &lt;code&gt;&amp;lt;Role&amp;gt;&lt;/code&gt; element into a plain string one day and an array the next. None of that is a coincidence — it's what happens when a converter uses a fixed, undocumented convention instead of a stated one. This guide walks through what that convention actually needs to cover, using a SOAP envelope as the running example, since legacy SOAP and WSDL interop is one of the most common reasons anyone reaches for an XML-to-JSON tool in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SOAP and WSDL interop keeps needing this conversion
&lt;/h2&gt;

&lt;p&gt;SOAP services, and the WSDL documents that describe them, are still very much alive in enterprise and government systems, banking middleware, and older internal APIs that nobody has had the time (or the budget) to migrate to REST. When a developer needs to inspect a SOAP response, write a test fixture against it, or mock its shape in a modern JavaScript or Python project, JSON is the natural working format — but hand-converting a real SOAP envelope quickly runs into exactly the details that trip up ad-hoc scripts: attributes, mixed text-and-element content, namespace prefixes, and elements that are sometimes singular and sometimes repeated.&lt;/p&gt;

&lt;p&gt;A WSDL file is worth calling out specifically, because the honest answer for &lt;code&gt;wsdl xml to json&lt;/code&gt; is more modest than it sounds: a WSDL document is just XML, so a general-purpose XML-to-JSON converter handles its structure fine — but it has no special understanding of what a &lt;code&gt;&amp;lt;wsdl:operation&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;wsdl:binding&amp;gt;&lt;/code&gt; &lt;em&gt;means&lt;/em&gt; semantically. That's a job for WSDL-aware tooling, not a format converter. Treating WSDL as generic, structurally-converted XML is the correct scope for this kind of tool, not a limitation to apologise for.&lt;/p&gt;

&lt;h2&gt;
  
  
  XML attributes to JSON: the &lt;code&gt;@&lt;/code&gt;-prefix convention
&lt;/h2&gt;

&lt;p&gt;Take a simple attributed element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;GetUserResponse&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Ada Lovelace&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/GetUserResponse&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most common lossless convention prefixes attribute names so they can never collide with a child element sharing the same name:&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;"GetUserResponse"&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;"@id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada Lovelace"&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;Some converters instead offer a "merge attributes as plain keys" option, dropping the &lt;code&gt;@&lt;/code&gt; for a cleaner-looking &lt;code&gt;{"id": "42", "Name": "..."}&lt;/code&gt;. That's a legitimate choice for simple documents, but it's genuinely lossy: if this element also had a child element literally named &lt;code&gt;&amp;lt;id&amp;gt;&lt;/code&gt;, the attribute and the child would overwrite each other in the merged JSON with no warning. A converter should let you choose, but should also tell you, on the page, exactly when that choice is risky.&lt;/p&gt;

&lt;h2&gt;
  
  
  Element text, and when it needs a &lt;code&gt;#text&lt;/code&gt; key
&lt;/h2&gt;

&lt;p&gt;A plain leaf element with only text content should collapse to a simple JSON value — there's no reason &lt;code&gt;&amp;lt;Name&amp;gt;Ada Lovelace&amp;lt;/Name&amp;gt;&lt;/code&gt; needs to become &lt;code&gt;{"Name": {"#text": "Ada Lovelace"}}&lt;/code&gt; when it has no attributes or children to also represent. The &lt;code&gt;#text&lt;/code&gt; key (or whatever marker you configure) only earns its place once an element has &lt;em&gt;both&lt;/em&gt; text content and attributes or children to sit alongside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Price&lt;/span&gt; &lt;span class="na"&gt;currency=&lt;/span&gt;&lt;span class="s"&gt;"GBP"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;42.50&lt;span class="nt"&gt;&amp;lt;/Price&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"Price"&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;"@currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"#text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"42.50"&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;Getting this collapse rule right is what keeps the common case — a document full of simple leaf elements — from turning into noisy, wrapper-object JSON that's unpleasant to work with downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Namespaces: keep the prefix, or strip it deliberately
&lt;/h2&gt;

&lt;p&gt;This is where &lt;code&gt;xml to json with namespaces&lt;/code&gt; queries usually run into trouble. A SOAP envelope is namespaced from the very first tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;soap:Envelope&lt;/span&gt; &lt;span class="na"&gt;xmlns:soap=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2003/05/soap-envelope"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;soap:Body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;GetUserResponse&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Ada Lovelace&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/GetUserResponse&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/soap:Body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/soap:Envelope&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lossless default is to keep the prefix exactly as written in the JSON key — &lt;code&gt;soap:Envelope&lt;/code&gt;, &lt;code&gt;soap:Body&lt;/code&gt; — rather than resolving it to the namespace URI it's declared against, or silently stripping it to a bare &lt;code&gt;Envelope&lt;/code&gt;. The &lt;code&gt;xmlns:soap&lt;/code&gt; declaration itself just becomes an ordinary &lt;code&gt;@xmlns:soap&lt;/code&gt; attribute on the element that declared it; it isn't specially resolved or merged across scopes. Stripping prefixes for a cleaner-looking key is a reasonable &lt;em&gt;option&lt;/em&gt; — plenty of people genuinely don't need the prefix for their downstream use — but it should be a visible toggle, not the only behaviour on offer, because two differently-prefixed elements sharing a local name would otherwise collapse into the same JSON key and silently overwrite each other.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
If you're converting a SOAP envelope specifically to inspect the &lt;em&gt;body&lt;/em&gt; payload and don't care about the envelope/header wrapper, it's usually faster to convert the whole document once with namespaces preserved, then just navigate into &lt;code&gt;soap:Body&lt;/code&gt; in the resulting JSON — rather than trying to strip the envelope out beforehand and risking a malformed fragment.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  XML arrays to JSON: repeated elements, not a fixed schema
&lt;/h2&gt;

&lt;p&gt;XML has no native array type — repetition is the only signal. When the same tag name appears more than once under the same parent, in document order, that's an array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;User&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Ada Lovelace&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Role&amp;gt;&lt;/span&gt;Admin&lt;span class="nt"&gt;&amp;lt;/Role&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Role&amp;gt;&lt;/span&gt;Analyst&lt;span class="nt"&gt;&amp;lt;/Role&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/User&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"User"&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;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada Lovelace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Role"&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;"Admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyst"&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;The detail that trips up naive converters: a single occurrence of &lt;code&gt;&amp;lt;Role&amp;gt;&lt;/code&gt; should stay a plain string, &lt;em&gt;not&lt;/em&gt; get force-wrapped into a one-item array just because the schema theoretically allows repetition. If it did, every consumer of the JSON would have to write &lt;code&gt;Array.isArray(user.Role) ? user.Role : [user.Role]&lt;/code&gt; defensively on every field that might repeat — which defeats the point of a clean conversion. Comments and processing instructions, meanwhile, are simply dropped; they were never part of the data, only the markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going the other way: JSON to XML online, correctly
&lt;/h2&gt;

&lt;p&gt;The reverse direction needs the literal inverse of every rule above, applied consistently:&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;"GetUserResponse"&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;"@id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada Lovelace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Role"&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;"Admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyst"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;GetUserResponse&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Ada Lovelace&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Role&amp;gt;&lt;/span&gt;Admin&lt;span class="nt"&gt;&amp;lt;/Role&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Role&amp;gt;&lt;/span&gt;Analyst&lt;span class="nt"&gt;&amp;lt;/Role&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/GetUserResponse&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keys matching your attribute prefix become attributes again, arrays repeat as sibling elements sharing the array's key as the tag name, and reserved XML characters (&lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;"&lt;/code&gt;, &lt;code&gt;'&lt;/code&gt;) need correct escaping in both element text and attribute values. If the root JSON value is a single-key object, that key can become the XML root tag directly; an array or a multi-key object needs an explicit root element name, since XML — unlike JSON — always needs exactly one top-level element.&lt;/p&gt;



&lt;p&gt;If you want to sanity-check that a JSON payload is well-formed before converting it to XML — particularly useful when you've hand-edited a fixture — a plain &lt;a href="https://dev.to/tools/json-formatter"&gt;JSON formatter and validator&lt;/a&gt; is worth running first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round-trip fidelity: why the settings have to be shared, not duplicated
&lt;/h2&gt;

&lt;p&gt;The real test of an XML ↔ JSON tool is whether &lt;code&gt;XML → JSON → XML&lt;/code&gt; actually reproduces your original document. That only holds if the attribute prefix, text-content key, namespace handling, and array rule are the &lt;em&gt;literal inverse&lt;/em&gt; of each other in both directions — which in practice means both directions need to read from one shared settings object, not two independently-built converters bolted together on the same page. A tool where "convert to JSON" and "convert to XML" were implemented separately, even by the same author, will drift the moment one side's default changes and the other doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it: an XML ↔ JSON converter built around round-trip fidelity
&lt;/h2&gt;

&lt;p&gt;We built Skojio's &lt;a href="https://dev.to/tools/xml-json-converter"&gt;XML ↔ JSON Converter&lt;/a&gt; specifically to make this convention visible, shared, and symmetric rather than fixed and hidden. It supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A configurable attribute prefix (&lt;code&gt;@&lt;/code&gt; or &lt;code&gt;$&lt;/code&gt;, or merge-as-plain-keys with an on-page warning about when that's lossy)&lt;/li&gt;
&lt;li&gt;A configurable text-content key, applied only when an element also has attributes or children&lt;/li&gt;
&lt;li&gt;Namespace prefixes kept by default (the lossless choice), with an optional strip-prefix toggle&lt;/li&gt;
&lt;li&gt;Repeated-sibling-to-array conversion that never force-wraps a single occurrence&lt;/li&gt;
&lt;li&gt;A built-in "Load SOAP example" button that seeds a representative, namespaced envelope so you can see the whole conversion in one click&lt;/li&gt;
&lt;li&gt;A one-click "Verify round-trip" action that sends your output straight back through the other direction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs in your browser via the native &lt;code&gt;DOMParser&lt;/code&gt; and hand-rolled JavaScript — nothing you paste or upload is sent to a server, and there's no external entity or DTD fetching, so there's no XXE risk either. It's a natural complement to &lt;a href="https://dev.to/tools/json-formatter"&gt;JSON Formatter &amp;amp; Validator&lt;/a&gt; for checking structure first, and to &lt;a href="https://dev.to/tools/json-csv-converter"&gt;JSON ↔ CSV Converter&lt;/a&gt; if the same data eventually needs to land in a spreadsheet instead of a legacy XML endpoint.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;SOAP and WSDL interop is one of the most common reasons developers need an XML ↔ JSON converter — treat WSDL as generic XML, not something requiring semantic understanding of its operations.&lt;/li&gt;
&lt;li&gt;XML attributes need a visible, consistent convention (commonly an &lt;code&gt;@&lt;/code&gt; prefix) to avoid silently colliding with child elements of the same name.&lt;/li&gt;
&lt;li&gt;Namespace prefixes should be kept by default (&lt;code&gt;soap:Envelope&lt;/code&gt;), not silently stripped or resolved — stripping is a lossy option, not the only behaviour.&lt;/li&gt;
&lt;li&gt;Repeated sibling elements become a JSON array in document order; a single occurrence must stay a plain value, never force-wrapped.&lt;/li&gt;
&lt;li&gt;Round-trip fidelity (XML → JSON → XML) only holds if both directions share one settings object rather than independently guessing at the same conventions.
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xml</category>
      <category>json</category>
      <category>soap</category>
      <category>dataconversion</category>
    </item>
    <item>
      <title>AGENTS.md, Explained: One File for Claude, Cursor, Copilot and Windsurf</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:05:04 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/agentsmd-explained-one-file-for-claude-cursor-copilot-and-windsurf-7dl</link>
      <guid>https://dev.to/skojiocommunity/agentsmd-explained-one-file-for-claude-cursor-copilot-and-windsurf-7dl</guid>
      <description>&lt;p&gt;If you use more than one AI coding agent on the same project, you've probably noticed the file sprawl: an &lt;code&gt;AGENTS.md&lt;/code&gt;, a &lt;code&gt;CLAUDE.md&lt;/code&gt;, a &lt;code&gt;.cursor/rules/&lt;/code&gt; directory, a &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;, maybe a &lt;code&gt;.windsurf/rules/&lt;/code&gt;. They all say roughly the same thing — how to build the project, what conventions to follow — and they all drift apart the moment you change one and forget the others.&lt;/p&gt;

&lt;p&gt;This post explains what &lt;code&gt;AGENTS.md&lt;/code&gt; is, why it exists, and how to keep the whole set in sync without hand-copying between formats.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AGENTS.md actually is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is a plain-Markdown file at your repository root that tells an AI coding agent what it needs to know about your project. There are no required fields and no special schema — just Markdown headings you'd recognise from any README:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Project overview&lt;/span&gt;
A Next.js + TypeScript application.

&lt;span class="gu"&gt;## Setup / install&lt;/span&gt;
npm install

&lt;span class="gu"&gt;## Build, test, and lint&lt;/span&gt;
Build: npm run build
Test: npm test
Lint: npm run lint

&lt;span class="gu"&gt;## Code style and conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; TypeScript strict mode — avoid &lt;span class="sb"&gt;`any`&lt;/span&gt; without good reason.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer server components; add "use client" only where needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes it more than a convention is adoption. In December 2025, &lt;code&gt;AGENTS.md&lt;/code&gt; was donated to the Linux Foundation's Agentic AI Foundation as an open standard, and it's now read natively by Claude Code, Codex, Cursor, Copilot, Gemini CLI, Windsurf, Zed, Aider and roughly thirty other agents. For most tools, a single good &lt;code&gt;AGENTS.md&lt;/code&gt; is all you need.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Because AGENTS.md has no required fields, the fastest way to write a good one is to start from a stack-appropriate skeleton — overview, setup, build/test/lint, conventions — and fill in your real commands. That's what the stack presets in the tool below do.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you still end up with five files
&lt;/h2&gt;

&lt;p&gt;If everything read &lt;code&gt;AGENTS.md&lt;/code&gt;, this would be a non-problem. Two things keep the sprawl alive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A few agents have their own established file.&lt;/strong&gt; Claude Code looks for &lt;code&gt;CLAUDE.md&lt;/code&gt;; Cursor has its &lt;code&gt;.cursor/rules/&lt;/code&gt; directory; older setups still carry a &lt;code&gt;.cursorrules&lt;/code&gt; or &lt;code&gt;.windsurfrules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor does something the others don't: per-file scoping.&lt;/strong&gt; A Cursor rule can be limited to specific globs, which is genuinely useful and has no equivalent in a flat file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So teams copy the same content into each file, and then the copies rot. A build command changes in &lt;code&gt;AGENTS.md&lt;/code&gt; and nobody updates &lt;code&gt;CLAUDE.md&lt;/code&gt;. A new convention lands in the Cursor rules and never makes it to Copilot. The files that were meant to keep your agents consistent quietly make them inconsistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix for CLAUDE.md: bridge, don't duplicate
&lt;/h2&gt;

&lt;p&gt;The most common mistake is pasting your whole &lt;code&gt;AGENTS.md&lt;/code&gt; into &lt;code&gt;CLAUDE.md&lt;/code&gt;. Don't. Claude Code supports importing another file with an &lt;code&gt;@&lt;/code&gt;-reference on the first line, so the recommended &lt;code&gt;CLAUDE.md&lt;/code&gt; is a one-line bridge plus only what's genuinely Claude-specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;@AGENTS.md

&lt;span class="gu"&gt;## Claude-specific overrides&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Prefer running the test suite with the focused &lt;span class="sb"&gt;`-t`&lt;/span&gt; flag during iteration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the shared instructions live in exactly one place. &lt;code&gt;CLAUDE.md&lt;/code&gt; inherits them through the &lt;code&gt;@AGENTS.md&lt;/code&gt; import and adds nothing it doesn't need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting Cursor rules to AGENTS.md (and the glob caveat)
&lt;/h2&gt;

&lt;p&gt;This is the conversion people search for most — "cursor rules to agents.md" — and it's the one with a real subtlety. A Cursor rule is an &lt;code&gt;.mdc&lt;/code&gt; file with YAML frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Testing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;conventions"&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;**&lt;/span&gt;&lt;span class="s"&gt;/*.test.ts, **/*.spec.ts&lt;/span&gt;
&lt;span class="na"&gt;alwaysApply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
Use Vitest. Co-locate tests with the code they cover.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Converting the &lt;em&gt;body&lt;/em&gt; to &lt;code&gt;AGENTS.md&lt;/code&gt; is trivial — it's already Markdown. The catch is the &lt;code&gt;globs&lt;/code&gt; line. That rule only applies to test files, but &lt;code&gt;AGENTS.md&lt;/code&gt; is flat: it applies to the whole project and has no way to express "only for &lt;code&gt;**/*.test.ts&lt;/code&gt;".&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
When a converter flattens a scoped Cursor rule, the scoping is lost. The honest thing to do is record the original glob verbatim — not drop it silently. A rule you &lt;em&gt;think&lt;/em&gt; is still scoped, but isn't, is worse than one you know is global.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;A good conversion therefore keeps the information visible. Instead of throwing the glob away, it appends the scoped rule under a labelled section and notes where it came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Scoped rules (originally Cursor-only)&lt;/span&gt;

&lt;span class="gu"&gt;### testing-conventions — Testing conventions&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Originally scoped to: **/*.test.ts, **/*.spec.ts&lt;/span&gt;

Use Vitest. Co-locate tests with the code they cover.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You keep the content, you can see exactly what was scoped, and you decide what to do about it — rather than a converter quietly pretending the scope survived.&lt;/p&gt;

&lt;h2&gt;
  
  
  .cursorrules and .windsurfrules are deprecated — read them, don't write them
&lt;/h2&gt;

&lt;p&gt;If you're still carrying a single-file &lt;code&gt;.cursorrules&lt;/code&gt; or &lt;code&gt;.windsurfrules&lt;/code&gt;, those formats are deprecated. Cursor moved to a &lt;code&gt;.cursor/rules/&lt;/code&gt; directory of &lt;code&gt;.mdc&lt;/code&gt; files; Windsurf moved to &lt;code&gt;.windsurf/rules/&lt;/code&gt;. The right migration is one-way: read the old single file to recover its content, then emit the current directory-based format. There's no reason to generate a new &lt;code&gt;.cursorrules&lt;/code&gt; in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it without hand-copying
&lt;/h2&gt;

&lt;p&gt;All of the above is mechanical, which means you shouldn't be doing it by hand. Skojio's &lt;a href="https://dev.to/tools/agents-md-generator"&gt;AGENTS.md Generator &amp;amp; Converter&lt;/a&gt; treats &lt;code&gt;AGENTS.md&lt;/code&gt; as the single source of truth and derives the rest from it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack presets&lt;/strong&gt; — Next.js + TypeScript, Python, Go, Rust, Node.js, or Generic — seed a sensible starting &lt;code&gt;AGENTS.md&lt;/code&gt; so you're editing, not staring at a blank file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live format tabs&lt;/strong&gt; show the CLAUDE.md bridge, Cursor &lt;code&gt;.mdc&lt;/code&gt; rules (with real frontmatter and multi-block scoping), Copilot and Windsurf equivalents as you type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import&lt;/strong&gt; an existing file in any of the formats — including deprecated &lt;code&gt;.cursorrules&lt;/code&gt; and &lt;code&gt;.windsurfrules&lt;/code&gt; — and convert to the others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The glob caveat is surfaced, not hidden:&lt;/strong&gt; convert a scoped Cursor rule to a flat format and you get the verbatim record shown above plus a plain-language warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy any file, or download all five at once&lt;/strong&gt; as a &lt;code&gt;.zip&lt;/code&gt; laid out at the correct paths for a drop-in.&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;It runs entirely in your browser — nothing you paste or upload is sent anywhere, which matters when the file you're editing describes your private repository. If you work with JSON alongside your agent config, the same in-browser, privacy-first approach powers Skojio's &lt;a href="https://dev.to/tools/json-formatter"&gt;JSON Formatter &amp;amp; Validator&lt;/a&gt; and &lt;a href="https://dev.to/tools/json-to-typescript"&gt;JSON to TypeScript Converter&lt;/a&gt;.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;AGENTS.md is an open standard (Linux Foundation, Dec 2025) read natively by ~30 AI coding agents — for most tools, one good AGENTS.md is enough.&lt;/li&gt;
&lt;li&gt;The sprawl persists because a few agents keep their own file (CLAUDE.md, Cursor rules) and Cursor adds per-file glob scoping the flat formats can't express.&lt;/li&gt;
&lt;li&gt;Write CLAUDE.md as a one-line &lt;code&gt;@AGENTS.md&lt;/code&gt; bridge plus overrides — never a duplicate that drifts.&lt;/li&gt;
&lt;li&gt;Converting scoped Cursor rules to a flat format loses the scoping; a good converter records the original glob verbatim instead of dropping it silently.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.cursorrules&lt;/code&gt; and &lt;code&gt;.windsurfrules&lt;/code&gt; are deprecated — read them to migrate, but emit the current &lt;code&gt;.cursor/rules/&lt;/code&gt; and &lt;code&gt;.windsurf/rules/&lt;/code&gt; directory formats.
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentsmd</category>
      <category>aicodingagents</category>
      <category>cursor</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>99.9% Uptime: How Much Downtime Is That, Really? (Plus Combined SLA)</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:05:35 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/999-uptime-how-much-downtime-is-that-really-plus-combined-sla-3i42</link>
      <guid>https://dev.to/skojiocommunity/999-uptime-how-much-downtime-is-that-really-plus-combined-sla-3i42</guid>
      <description>&lt;p&gt;"99.9% uptime" sounds like a small, almost meaningless gap from perfect. In practice it's the difference between an outage nobody notices and one that ends up in a postmortem. Whether you're reading a vendor's SLA, writing an incident report, or trying to work out what your app's &lt;em&gt;real&lt;/em&gt; availability is once you account for the database, the CDN, and the auth provider it depends on, the maths behind "the nines" is simple once you see it laid out — and easy to get subtly wrong (most people average SLAs together when they should be multiplying them). This is a walkthrough of both.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;99.9% uptime ("three nines") allows about &lt;strong&gt;8 hours 45 minutes 36 seconds&lt;/strong&gt; of downtime a year on the standard 365-day convention&lt;/li&gt;
&lt;li&gt;Each extra nine cuts permitted downtime by roughly a factor of ten&lt;/li&gt;
&lt;li&gt;For services that depend on each other, &lt;strong&gt;multiply availabilities together — never average them&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Two services each at 99.9% combine to about &lt;strong&gt;99.8%&lt;/strong&gt;, not 99.9% — a real, common mistake&lt;/li&gt;
&lt;li&gt;The year/month length convention you use (365 vs 365.25 days, fixed vs calendar-average month) changes every downtime figure, so check it before comparing two SLA calculators
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Does 99.9% Uptime Actually Mean?
&lt;/h2&gt;

&lt;p&gt;"99.9% uptime" is a percentage of &lt;em&gt;time&lt;/em&gt;, not a vague quality score. To turn it into something concrete, take the length of the reporting period and multiply it by the 0.1% that's allowed to be down. On the 365-day year / calendar-average month convention (30.44 days — the convention this calculator defaults to, since it's the one most billing and monitoring periods actually use):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Downtime allowed at 99.9%&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;1 minute 26 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;td&gt;10 minutes 5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;43 minutes 50 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;2 hours 11 minutes 24 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yearly&lt;/td&gt;
&lt;td&gt;8 hours 45 minutes 36 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
These numbers move if the convention moves. A 365.25-day year (accounting for leap years) or a fixed 30-day month instead of the 30.44-day calendar average both produce slightly different figures — which is exactly why two SLA calculators can disagree on "the same" percentage without either being wrong. Check which convention a vendor's number assumes before you compare it to your own monitoring dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The Nines, From One to Six
&lt;/h2&gt;

&lt;p&gt;Each additional "nine" in an uptime percentage cuts the permitted downtime by roughly a factor of ten. Here's the full reference table, again on the 365-day convention:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;th&gt;Common name&lt;/th&gt;
&lt;th&gt;Downtime per year&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;one nine&lt;/td&gt;
&lt;td&gt;about 36 days 12 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;one and a half nines&lt;/td&gt;
&lt;td&gt;about 18 days 6 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;two nines&lt;/td&gt;
&lt;td&gt;about 3 days 15 hours 36 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.5%&lt;/td&gt;
&lt;td&gt;two and a half nines&lt;/td&gt;
&lt;td&gt;about 1 day 19 hours 48 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;three nines&lt;/td&gt;
&lt;td&gt;8 hours 45 minutes 36 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.95%&lt;/td&gt;
&lt;td&gt;three and a half nines&lt;/td&gt;
&lt;td&gt;4 hours 22 minutes 48 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.99%&lt;/td&gt;
&lt;td&gt;four nines&lt;/td&gt;
&lt;td&gt;52 minutes 34 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.995%&lt;/td&gt;
&lt;td&gt;four and a half nines&lt;/td&gt;
&lt;td&gt;26 minutes 17 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.999%&lt;/td&gt;
&lt;td&gt;five nines&lt;/td&gt;
&lt;td&gt;5 minutes 15 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.9999%&lt;/td&gt;
&lt;td&gt;six nines&lt;/td&gt;
&lt;td&gt;about 32 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five nines gets quoted constantly as the gold standard for critical infrastructure — telecoms switches, payment rails — but it's worth noticing how little slack it leaves: barely five minutes a year across &lt;em&gt;every&lt;/em&gt; dependency, not just the headline service.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Convert an Outage Into an Uptime Percentage
&lt;/h2&gt;

&lt;p&gt;The forward direction (percentage → downtime) is the easy half. The other direction comes up just as often: you had an outage, and you need to report what percentage uptime that represents — for a postmortem, an SLA breach claim, or just your own dashboard.&lt;/p&gt;

&lt;p&gt;The maths is the mirror image of the table above: divide the downtime by the length of the period, subtract that fraction from 1, and multiply by 100. A 42-minute outage in a 30-day month, for instance, works out to roughly 99.903% uptime for that month — comfortably inside a 99.9% SLA, but worth checking against the exact contractual period length rather than eyeballing it.&lt;/p&gt;

&lt;p&gt;For odd, non-calendar windows — "we were down from 14:32 to 15:14 on the 12th" — a fixed day/week/month bucket doesn't apply cleanly. A custom date-range mode that takes an exact start and end timestamp plus the outage duration avoids rounding a postmortem window into the nearest calendar period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combined SLA for Dependent Services: Why You Can't Just Average
&lt;/h2&gt;

&lt;p&gt;This is the part most SLA discussions skip, and it's where the real mistakes happen. If your application depends on a database, a CDN, and an auth provider — and any one of them failing takes the whole app down — the application's &lt;em&gt;actual&lt;/em&gt; achievable uptime isn't the average of the three SLAs. It's their &lt;strong&gt;product&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two services each individually rated at 99.9% don't combine to 99.9%. They combine to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.999 × 0.999 = 0.998001 → 99.8001% ≈ 99.8%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's roughly &lt;strong&gt;17.5 hours&lt;/strong&gt; of downtime a year — about double what either service alone would suggest — simply because both need to be up at the same time, and each has its own independent chance of failing.&lt;/p&gt;

&lt;p&gt;Add a third hard dependency and it compounds further. Three services individually rated at 99.9%, 99.99%, and 99.95% combine to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.999 × 0.9999 × 0.9995 ≈ 0.998401 → 99.84%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— about &lt;strong&gt;14 hours&lt;/strong&gt; of downtime a year, worse than any single link in the chain, even though two of those three services individually look excellent on paper.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
This only applies to &lt;strong&gt;serial, hard dependencies&lt;/strong&gt; — where the whole system genuinely fails if any one component fails. It does &lt;strong&gt;not&lt;/strong&gt; apply to redundant or failover (parallel) architectures, where a single component going down doesn't take the system with it. Multiplying availabilities for a redundant setup would understate its real resilience, not overstate it — different maths for a different architecture.&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;
  
  
  Comparing Vendor SLAs Side by Side
&lt;/h2&gt;

&lt;p&gt;The last piece worth flagging: when you're comparing cloud providers or hosting plans — "AWS's 99.99% vs this vendor's 99.95%" — don't stop at the headline percentage. Line up the actual downtime figures for the periods that matter to you (monthly is usually the one that maps to billing credits), and check both vendors are quoting the same year/month convention. A 0.04 percentage-point gap sounds trivial until you see it's the difference between about 4 minutes and about 22 minutes of permitted downtime a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Doing any of this arithmetic by hand is fiddly and easy to get subtly wrong — particularly the averaging-vs-multiplying trap above, which even experienced engineers reach for under time pressure. Skojio's &lt;a href="https://dev.to/tools/sla-uptime-calculator"&gt;SLA uptime calculator&lt;/a&gt; covers all five directions in one tool: forward (percentage → downtime), reverse (outage → percentage), a custom date-range mode for postmortems, a compound/chained calculator that multiplies dependent services correctly, and a side-by-side comparison table for vendor SLAs — with the year/month convention exposed as an explicit, switchable toggle rather than silently hardcoded. It runs entirely in your browser: nothing you type ever leaves your device.&lt;/p&gt;

&lt;p&gt;If SLA maths brought you here, a few of Skojio's other free browser-based infra tools are worth a look too: the &lt;a href="https://dev.to/tools/chmod-calculator"&gt;Chmod Calculator&lt;/a&gt; for the other everyday Linux/Unix arithmetic, the &lt;a href="https://dev.to/tools/cron-expression-helper"&gt;Cron Expression Helper&lt;/a&gt; for scheduling maintenance windows around whatever downtime budget you've got left, and the &lt;a href="https://dev.to/tools/timezone-helper"&gt;Timezone Helper&lt;/a&gt; for making sure "this month" means the same thing to everyone on a distributed team.&lt;/p&gt;

</description>
      <category>sre</category>
      <category>devops</category>
      <category>sla</category>
      <category>uptime</category>
    </item>
    <item>
      <title>How to Generate TypeScript Types from JSON (Without Getting Optional Fields Wrong)</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:05:03 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/how-to-generate-typescript-types-from-json-without-getting-optional-fields-wrong-4nk0</link>
      <guid>https://dev.to/skojiocommunity/how-to-generate-typescript-types-from-json-without-getting-optional-fields-wrong-4nk0</guid>
      <description>&lt;p&gt;If you've ever pasted a single JSON object into a "json to typescript" tool and gotten back a clean &lt;code&gt;interface&lt;/code&gt;, only to have your API return a slightly different shape the next day and blow up your build — you've hit the single-sample problem. It's the most common, least talked-about failure mode in JSON-to-TypeScript conversion, and it's worth understanding before you trust any generated type in a real codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "generate TypeScript types from JSON" actually means
&lt;/h2&gt;

&lt;p&gt;At its core, converting JSON to TypeScript is a structural mapping exercise. JSON has a small set of value types — string, number, boolean, null, array, object — and each one has an obvious TypeScript counterpart:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada Lovelace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"active"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a single, simple object, that's the whole job. The complexity — and the part where converters genuinely differ in quality — shows up the moment your JSON sample is an &lt;strong&gt;array of objects&lt;/strong&gt;, which is the shape almost every real REST API response actually has.&lt;/p&gt;

&lt;h2&gt;
  
  
  The single-sample problem: why optional fields go missing
&lt;/h2&gt;

&lt;p&gt;Here's the trap. Say your API returns a list of users, and one of them is missing an &lt;code&gt;email&lt;/code&gt; field because they signed up via SSO:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada"&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="s2"&gt;"ada@example.com"&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;"id"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alan"&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;A converter that only looks at the first array element will produce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's wrong — &lt;code&gt;email&lt;/code&gt; is not guaranteed to be present, but the generated type says it is. The bug won't show up immediately. It shows up later, as a runtime &lt;code&gt;undefined&lt;/code&gt; where TypeScript swore a &lt;code&gt;string&lt;/code&gt; would be, because nothing forced the compiler to check the field that was actually missing in your sample data.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
This is the single biggest correctness gap across the free "json to typescript interface generator" tools you'll find in a search: most infer structure from element 0 of an array (or the one object you pasted) and never look at the rest.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What correct multi-sample merging looks like
&lt;/h2&gt;

&lt;p&gt;The fix is straightforward in concept, if fiddly to implement correctly: when the JSON root is an array of similar objects, merge the &lt;strong&gt;union of every key seen across every element&lt;/strong&gt; — not just the first — into one interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// absent from at least one sample&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same logic applies to types, not just presence. If a field is a &lt;code&gt;number&lt;/code&gt; in one record and a &lt;code&gt;string&lt;/code&gt; in another — a common pattern when an API is mid-migration, or a field is sometimes an ID and sometimes a slug — proper merging produces 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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forty-two"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A converter that samples only the first element would type &lt;code&gt;value&lt;/code&gt; as &lt;code&gt;number&lt;/code&gt; and quietly break the moment it saw a string. This matters just as much for &lt;strong&gt;nested arrays of objects&lt;/strong&gt;: if a &lt;code&gt;tags&lt;/code&gt; array inside one record has an item missing a &lt;code&gt;weight&lt;/code&gt; field, that optionality has to survive being merged again when the outer records themselves get merged together — a subtlety that's easy to get right for a single level of nesting and easy to lose once you go two levels deep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling JSON to TypeScript nested objects
&lt;/h2&gt;

&lt;p&gt;Nested objects raise a second, separate design question: should the nested shape be written inline, or pulled out into its own named interface?&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;"id"&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;"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;"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;"London"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"postcode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SW1A 1AA"&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;&lt;strong&gt;Inline&lt;/strong&gt; keeps everything in one block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Extracted&lt;/strong&gt; gives the nested shape its own name, which is usually more readable and lets you reuse the type elsewhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extraction is generally the better default for anything beyond a trivial one-off shape, especially if the same nested structure appears under more than one property (a &lt;code&gt;billingAddress&lt;/code&gt; and &lt;code&gt;shippingAddress&lt;/code&gt; with identical fields, for instance) — a good tool should deduplicate those into a single interface rather than generating two identical ones with different names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other details worth checking before you trust a converter's output
&lt;/h2&gt;

&lt;p&gt;A handful of smaller rules separate a converter that produces compiling, correct TypeScript from one that produces something that merely &lt;em&gt;looks&lt;/em&gt; right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Empty arrays&lt;/strong&gt; should infer as &lt;code&gt;unknown[]&lt;/code&gt;, not silently &lt;code&gt;any[]&lt;/code&gt; — an empty array genuinely tells you nothing about its element type, and &lt;code&gt;any&lt;/code&gt; defeats the entire point of generating types in the first place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keys that aren't valid TypeScript identifiers&lt;/strong&gt; — hyphens, spaces, a leading digit, reserved words — need automatic quoting (&lt;code&gt;"last-login": string;&lt;/code&gt;), or the output won't compile at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;null&lt;/code&gt; values&lt;/strong&gt; should be handled explicitly, either as an honest &lt;code&gt;T | null&lt;/code&gt; union or folded into &lt;code&gt;key?: T&lt;/code&gt; depending on your team's convention — never silently dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;export&lt;/code&gt; and &lt;code&gt;readonly&lt;/code&gt;&lt;/strong&gt; should be simple toggles you control, not a hard-coded style choice you have to find-and-replace after the fact.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  Try it: a json to typescript converter built around correct merging
&lt;/h2&gt;

&lt;p&gt;Skojio's &lt;a href="https://dev.to/tools/json-to-typescript"&gt;JSON to TypeScript Converter&lt;/a&gt; exists specifically to close the single-sample gap described above. Paste a JSON object or an array of objects and it will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merge the union of keys across &lt;strong&gt;every&lt;/strong&gt; array element (not just the first) to correctly infer optional fields and unions&lt;/li&gt;
&lt;li&gt;Recursively apply the same merging to nested arrays of objects, so optionality survives being merged at multiple levels&lt;/li&gt;
&lt;li&gt;Let you toggle &lt;code&gt;interface&lt;/code&gt; vs &lt;code&gt;type&lt;/code&gt;, extract-named-interfaces vs inline nesting, &lt;code&gt;export&lt;/code&gt;, and &lt;code&gt;readonly&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Automatically quote invalid-identifier keys, with a visible note when it happens&lt;/li&gt;
&lt;li&gt;Give you a copy button and a one-click &lt;code&gt;.ts&lt;/code&gt; download&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs in your browser — nothing you paste is sent anywhere. If you want to check your JSON's shape is well-formed before generating types from it, run it through &lt;a href="https://dev.to/tools/json-formatter"&gt;JSON Formatter &amp;amp; Validator&lt;/a&gt; first; if you'd rather get the same data into tabular CSV instead of TypeScript, &lt;a href="https://dev.to/tools/json-csv-converter"&gt;JSON ↔ CSV Converter&lt;/a&gt; is the same-input, different-output sibling tool.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Most free JSON-to-TypeScript converters infer structure from a single sample (often just array element 0), which silently mis-types fields that are sometimes missing or sometimes a different type.&lt;/li&gt;
&lt;li&gt;Correct multi-sample merging unions the keys across every array element: absent-in-some-records becomes &lt;code&gt;key?: T&lt;/code&gt;, differently-typed-across-records becomes a union type.&lt;/li&gt;
&lt;li&gt;Nested arrays of objects need the same merge logic applied recursively — optionality discovered one level down has to survive being merged again at the level above.&lt;/li&gt;
&lt;li&gt;Extracting nested objects into named, deduplicated interfaces is usually more useful than writing them inline, especially when the same shape appears under more than one property.&lt;/li&gt;
&lt;li&gt;Empty arrays should infer as &lt;code&gt;unknown[]&lt;/code&gt;, not &lt;code&gt;any[]&lt;/code&gt;; invalid-identifier keys need automatic quoting, not a compile error.
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>json</category>
      <category>typescript</category>
      <category>dataconversion</category>
      <category>developertools</category>
    </item>
    <item>
      <title>How to Flatten Nested JSON to CSV (and Convert It Back Without Losing Data)</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:05:02 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/how-to-flatten-nested-json-to-csv-and-convert-it-back-without-losing-data-21n3</link>
      <guid>https://dev.to/skojiocommunity/how-to-flatten-nested-json-to-csv-and-convert-it-back-without-losing-data-21n3</guid>
      <description>&lt;p&gt;Ask anyone who has piped an API response into a spreadsheet for a stakeholder how it went, and you'll usually hear the same complaint: it worked fine until the JSON had a nested object, an array, or a comma inside a text field, and then the CSV came out wrong — misaligned columns, mangled quotes, or every value turned into text. This guide covers what actually needs to happen underneath a JSON-to-CSV conversion (and the reverse), so you can either build it correctly yourself or know exactly what to look for in a tool that claims to handle it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nested JSON breaks naive CSV export
&lt;/h2&gt;

&lt;p&gt;CSV is fundamentally a flat, two-dimensional format — rows and columns, nothing more. JSON has no such restriction: values can be objects, arrays, or arrays of objects, nested arbitrarily deep. The moment you try to represent &lt;code&gt;{"user": {"name": "Ada", "address": {"city": "London"}}}&lt;/code&gt; as a spreadsheet row, you have to make a decision JSON never forced you to make: how does a deeply nested value become a single flat cell, or a set of flat cells?&lt;/p&gt;

&lt;p&gt;Most quick scripts skip this decision entirely and either crash on nested data, silently drop it, or &lt;code&gt;JSON.stringify()&lt;/code&gt; the whole nested value into one unreadable cell without telling you that's what happened. None of those are wrong, exactly — but they're all &lt;em&gt;silent&lt;/em&gt;, and silent conventions are exactly what breaks the moment someone else tries to read the CSV back, or tries to convert it back to JSON six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flattening nested objects: pick a separator, then be consistent
&lt;/h2&gt;

&lt;p&gt;The standard approach for a nested &lt;strong&gt;object&lt;/strong&gt; is dot-notation flattening: each level of nesting joins onto the column name with a separator.&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada Lovelace"&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;"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;"London"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postcode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SW1A 1AA"&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;becomes three flat columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name,address.city,address.postcode
Ada Lovelace,London,SW1A 1AA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separator itself (&lt;code&gt;.&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, or &lt;code&gt;/&lt;/code&gt;) doesn't matter much on its own — what matters is that you use the &lt;em&gt;same&lt;/em&gt; separator every time, and that whatever reads the CSV back into JSON knows which separator was used. A converter that lets you flatten with &lt;code&gt;.&lt;/code&gt; today and expects &lt;code&gt;_&lt;/code&gt; on the way back will quietly produce the wrong nested structure, which is a much harder bug to spot than a crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested arrays: the decision most tools get wrong
&lt;/h2&gt;

&lt;p&gt;Objects flatten cleanly because they have named keys. Arrays don't — and this is where "flatten JSON to CSV" tools genuinely diverge, because there are two legitimate approaches and no tool should silently pick one for you without saying so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Index-expand.&lt;/strong&gt; Each array item becomes its own numbered column set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name,tags.0,tags.1,tags.2
Ada Lovelace,mathematician,writer,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is great for short, roughly fixed-length arrays — tags, categories, a small list of phone numbers — because every item gets its own real spreadsheet column that's easy to filter and sort on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep-as-JSON-text.&lt;/strong&gt; The whole array stays as one JSON-encoded string in a single cell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name,tags
Ada Lovelace,"[""mathematician"",""writer""]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserves the array exactly — including its length — but you lose the ability to work with individual items as separate spreadsheet columns.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
If your arrays vary a lot in length between records, index-expand will pad the shorter ones with empty (or &lt;code&gt;null&lt;/code&gt;) trailing columns rather than telling you "this record only had two tags, not three." That's a structural limit of flattening arrays into columns at all, not a bug in any particular tool — if the exact length of a variable array matters to your downstream process, keep-as-JSON-text is the honest choice.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Neither option is objectively better. What matters is that the tool you use makes the choice &lt;strong&gt;visible and consistent&lt;/strong&gt;, rather than burying it in a settings drawer or hard-coding one convention with no way to change it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom delimiters: comma isn't universal
&lt;/h2&gt;

&lt;p&gt;A plain comma is the default CSV delimiter, but it's not the only one you'll meet in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semicolon&lt;/strong&gt; — common in locales where a comma is the decimal separator (much of continental Europe), so Excel and other spreadsheet tools there export semicolon-delimited files by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tab&lt;/strong&gt; (TSV) — a frequent choice for exports where field values themselves often contain commas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipe (&lt;code&gt;|&lt;/code&gt;)&lt;/strong&gt; — common in older systems and log-style exports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever delimiter you use, the quoting rules (RFC 4180) still apply: any field containing the delimiter, a double quote, or a line break needs to be wrapped in quotes, with internal quotes doubled. A converter that lets you switch delimiters but forgets to re-check which characters now need quoting will produce CSV that looks fine until you open it in the one spreadsheet program that parses it strictly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going the other way: CSV to nested JSON, with real types
&lt;/h2&gt;

&lt;p&gt;Converting CSV back to JSON has its own pitfalls, mostly because every CSV cell is, technically, just a string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type inference&lt;/strong&gt; turns those strings back into real JSON types where it's safe to do so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;42&lt;/code&gt;, &lt;code&gt;-3.5&lt;/code&gt; → JSON numbers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt; (any letter case) → JSON booleans&lt;/li&gt;
&lt;li&gt;an empty cell → &lt;code&gt;null&lt;/code&gt; (or an empty string, depending on your setting)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classic trap is &lt;strong&gt;leading zeros&lt;/strong&gt;. A postcode like &lt;code&gt;"007"&lt;/code&gt; or a reference ID stored as text will silently become the number &lt;code&gt;7&lt;/code&gt; if a converter blindly coerces every numeric-looking string — which is precisely the kind of quiet corruption that surfaces as a support ticket weeks later. A converter with real type inference protects leading-zero strings and gives you a "force all strings" override for cases where you don't want any coercion at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nested-key reconstruction&lt;/strong&gt; is simply flattening in reverse: an &lt;code&gt;address.city&lt;/code&gt; column becomes a nested &lt;code&gt;address&lt;/code&gt; object with a &lt;code&gt;city&lt;/code&gt; key, and — if the array convention was index-expand — &lt;code&gt;tags.0&lt;/code&gt;/&lt;code&gt;tags.1&lt;/code&gt; columns rebuild back into a &lt;code&gt;tags&lt;/code&gt; array.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round-trip fidelity: why the settings have to match on both sides
&lt;/h2&gt;

&lt;p&gt;Put the two directions together and you get the real test of a JSON ↔ CSV tool: does &lt;code&gt;JSON → CSV → JSON&lt;/code&gt; actually reproduce your original structure? This only works if the flatten and unflatten logic are literal inverses of each other — same separator, same array-handling choice, on both sides. If a tool's export and import conventions were built independently (or if you export from one tool and import with another that guesses differently), you'll get valid-looking JSON back that quietly doesn't match what you started with.&lt;/p&gt;



&lt;p&gt;If you want to sanity-check the &lt;em&gt;shape&lt;/em&gt; of your JSON before you flatten it — spotting an unexpectedly nested field, or validating that an API response is well-formed in the first place — a plain &lt;a href="https://dev.to/tools/json-formatter"&gt;JSON formatter and validator&lt;/a&gt; is worth running first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it: a converter built around round-trip fidelity
&lt;/h2&gt;

&lt;p&gt;We built Skojio's &lt;a href="https://dev.to/tools/json-csv-converter"&gt;JSON ↔ CSV Converter&lt;/a&gt; specifically to make the flatten/unflatten convention visible and symmetric instead of hidden. It supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comma, semicolon, tab, pipe, or a custom delimiter, with correct RFC 4180 quoting on both read and write&lt;/li&gt;
&lt;li&gt;A persistent, always-visible "Nested data" control for the separator and array strategy — the one setting that determines whether your round trip actually holds&lt;/li&gt;
&lt;li&gt;Type inference on the CSV → JSON path, with leading-zero protection and a "force all strings" override&lt;/li&gt;
&lt;li&gt;A one-click "Verify round-trip" action that sends your output straight back through the other direction, so you can see your original structure reappear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs in your browser — nothing you paste or upload is sent to a server. It's a genuinely useful complement to &lt;a href="https://dev.to/tools/json-formatter"&gt;JSON Formatter&lt;/a&gt; for validating structure first, and to &lt;a href="https://dev.to/tools/base64"&gt;Base64 Encoder/Decoder&lt;/a&gt; if your JSON payload also needs encoding for a data URI or an API header along the way.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;CSV is flat; JSON isn't — every JSON-to-CSV conversion has to make an explicit decision about how nesting becomes columns.&lt;/li&gt;
&lt;li&gt;Nested objects flatten cleanly with a consistent separator; nested arrays need an explicit index-expand vs keep-as-JSON-text choice.&lt;/li&gt;
&lt;li&gt;Custom delimiters (semicolon, tab, pipe) need the same RFC 4180 quoting rules as a plain comma.&lt;/li&gt;
&lt;li&gt;Type inference on CSV → JSON must protect leading-zero strings from being coerced into numbers.&lt;/li&gt;
&lt;li&gt;Round-trip fidelity only holds if the same separator and array-strategy settings are used on both sides of the conversion.
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>json</category>
      <category>csv</category>
      <category>dataconversion</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Chmod 755 vs 644 vs 777: What Linux File Permissions Actually Mean</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Tue, 07 Jul 2026 10:05:03 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/chmod-755-vs-644-vs-777-what-linux-file-permissions-actually-mean-15bf</link>
      <guid>https://dev.to/skojiocommunity/chmod-755-vs-644-vs-777-what-linux-file-permissions-actually-mean-15bf</guid>
      <description>&lt;p&gt;If you've ever pasted &lt;code&gt;chmod 755&lt;/code&gt; from a Stack Overflow answer without knowing why, or stared at &lt;code&gt;rwxr-xr-x&lt;/code&gt; in an &lt;code&gt;ls -l&lt;/code&gt; listing wondering what it's telling you, you're not alone. Linux file permissions are one of those things every developer touches constantly and understands properly only after getting burned once. This guide walks through what the numbers actually mean, when 755 is right and when 644 is right, why 777 keeps showing up in "quick fixes" that aren't actually fixes, and how to decode a permission string the moment you see one.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Each permission digit (0–7) is the sum of read (4), write (2), and execute (1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;644&lt;/strong&gt; — standard for regular files: owner can read/write, everyone else read-only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;755&lt;/strong&gt; — standard for directories and scripts: owner has full access, everyone else can read and execute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;777&lt;/strong&gt; — full access for everyone, almost never the correct answer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rwxr-xr-x&lt;/code&gt; is the symbolic spelling of the same permission set &lt;code&gt;ls -l&lt;/code&gt; shows you — and it decodes to 755
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Octal Permission Numbers Work
&lt;/h2&gt;

&lt;p&gt;Every Linux file or directory has three sets of permissions: one for the &lt;strong&gt;owner&lt;/strong&gt;, one for the &lt;strong&gt;group&lt;/strong&gt;, and one for &lt;strong&gt;everyone else&lt;/strong&gt;. Each set is a single digit from 0 to 7, built by adding together the values of whichever permissions are switched on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;execute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a digit of &lt;code&gt;7&lt;/code&gt; means read + write + execute (4+2+1), &lt;code&gt;6&lt;/code&gt; means read + write (4+2, no execute), &lt;code&gt;5&lt;/code&gt; means read + execute (4+1, no write), and &lt;code&gt;0&lt;/code&gt; means nothing at all. A three-digit permission like &lt;code&gt;755&lt;/code&gt; is just three of these digits stacked: &lt;code&gt;7&lt;/code&gt; for the owner, &lt;code&gt;5&lt;/code&gt; for the group, &lt;code&gt;5&lt;/code&gt; for everyone else.&lt;/p&gt;

&lt;p&gt;A fourth, leading digit is optional and encodes the special bits — setuid, setgid, and the sticky bit — which we'll come back to further down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chmod 755 vs 644: When to Use Each
&lt;/h2&gt;

&lt;p&gt;These two are by far the most common permission sets you'll ever set, and the difference between them is entirely the execute bit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Other&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;644&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;read, write&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;read&lt;/td&gt;
&lt;td&gt;Regular files: HTML, CSS, images, config files, documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;755&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;read, write, execute&lt;/td&gt;
&lt;td&gt;read, execute&lt;/td&gt;
&lt;td&gt;read, execute&lt;/td&gt;
&lt;td&gt;Directories, shell scripts, compiled binaries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
As a rule of thumb: &lt;strong&gt;644 for files, 755 for directories and anything meant to run.&lt;/strong&gt; A directory needs its execute bit set just to be entered (&lt;code&gt;cd&lt;/code&gt;) or listed properly — without it, even a directory you can "read" will behave strangely.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;If a hosting support article tells you to "set permissions to 644", it means: the owner (usually your account or the web server user) can edit the file, and everyone else — including the web server process reading it to serve a page — can only view it. That's exactly right for a static file. Try to make that same file executable and nothing changes for how it's served; the execute bit is irrelevant to files a web server just reads and returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why chmod 777 Is (Almost) Always the Wrong Answer
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;chmod 777&lt;/code&gt; gives &lt;strong&gt;every user on the system&lt;/strong&gt; — owner, group, and anyone else who can reach the file — full read, write, and execute access. On a shared server or anything reachable by a web process, that means any compromised script, any other user account, or any misconfigured process can read, overwrite, or execute that file freely.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;The reason 777 shows up so often in forum "fixes" is that it genuinely does stop the permission-denied error — by removing every restriction, it removes the specific one that was actually the problem too. Almost always, the real fix is narrower: correcting file &lt;strong&gt;ownership&lt;/strong&gt; (&lt;code&gt;chown&lt;/code&gt;) so the right user owns the file, or fixing &lt;strong&gt;group membership&lt;/strong&gt; so the process that needs access has it through the group permission instead of blowing the doors off for everyone. &lt;code&gt;750&lt;/code&gt; or &lt;code&gt;770&lt;/code&gt; — owner and group get full or read+execute access, "other" gets nothing — covers the overwhelming majority of cases people reach for 777 to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symbolic Notation: What Does rwxr-xr-x Mean?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ls -l&lt;/code&gt; doesn't show you octal digits — it shows you nine characters split into three groups of three, one group per owner/group/other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rwxr-xr-x
└┬┘└┬┘└┬┘
owner group other
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each group is &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;w&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt; if that permission is granted, or &lt;code&gt;-&lt;/code&gt; if it isn't, always in that fixed order. So &lt;code&gt;rwx&lt;/code&gt; is 7, &lt;code&gt;r-x&lt;/code&gt; is 5, &lt;code&gt;rw-&lt;/code&gt; is 6, and &lt;code&gt;r--&lt;/code&gt; is 4 — the same values from the octal table above, just spelled out instead of summed. &lt;code&gt;rwxr-xr-x&lt;/code&gt; is therefore owner=7, group=5, other=5: octal &lt;code&gt;755&lt;/code&gt;. &lt;code&gt;rw-r--r--&lt;/code&gt; is 6, 4, 4: octal &lt;code&gt;644&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Full ls -l Output
&lt;/h2&gt;

&lt;p&gt;A real &lt;code&gt;ls -l&lt;/code&gt; line carries more than just the permission string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;-rwxr-xr-x  1 alice staff  8192 Jun 30 09:14 deploy.sh
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking that down left to right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-&lt;/code&gt;&lt;/strong&gt; — the file type. &lt;code&gt;-&lt;/code&gt; is a regular file, &lt;code&gt;d&lt;/code&gt; is a directory, &lt;code&gt;l&lt;/code&gt; is a symbolic link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rwxr-xr-x&lt;/code&gt;&lt;/strong&gt; — the nine-character permission string covered above (755 here).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;1&lt;/code&gt;&lt;/strong&gt; — the hard-link count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;alice&lt;/code&gt;&lt;/strong&gt; — the owning user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;staff&lt;/code&gt;&lt;/strong&gt; — the owning group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;8192&lt;/code&gt;&lt;/strong&gt; — file size in bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Jun 30 09:14&lt;/code&gt;&lt;/strong&gt; — last modified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deploy.sh&lt;/code&gt;&lt;/strong&gt; — the filename.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you know the shape, you can decode any &lt;code&gt;ls -l&lt;/code&gt; line at a glance — or paste the whole line into a parser and let it do the arithmetic for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setuid, Setgid, and the Sticky Bit — the Fourth Digit
&lt;/h2&gt;

&lt;p&gt;Beyond the standard three digits, there's an optional fourth digit that sets three special behaviours: &lt;strong&gt;setuid&lt;/strong&gt; (4) makes an executable run with its owner's privileges rather than the caller's; &lt;strong&gt;setgid&lt;/strong&gt; (2) does the same for group identity and, on a directory, makes new files inherit that directory's group automatically; and the &lt;strong&gt;sticky bit&lt;/strong&gt; (1) — used on shared directories like &lt;code&gt;/tmp&lt;/code&gt; — means only a file's own owner (or root) can delete or rename it, even if everyone else can write there.&lt;/p&gt;

&lt;p&gt;These show up in symbolic notation as a lowercase &lt;code&gt;s&lt;/code&gt; or &lt;code&gt;t&lt;/code&gt; replacing the execute character when the underlying execute bit is also on, or an uppercase &lt;code&gt;S&lt;/code&gt;/&lt;code&gt;T&lt;/code&gt; when it isn't (an unusual combination &lt;code&gt;ls -l&lt;/code&gt; flags this way on purpose, since it's usually a mistake). It's a small enough detail that it's easier to see live than to memorise — worth playing with rather than reading about.&lt;/p&gt;



&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The fastest way to get comfortable with any of this is to stop doing the arithmetic in your head. Skojio's &lt;a href="https://dev.to/tools/chmod-calculator"&gt;chmod calculator&lt;/a&gt; keeps the octal number, the symbolic string, and a checkbox grid all in sync as you edit any one of them, generates the exact &lt;code&gt;chmod&lt;/code&gt; command to copy into a terminal, and — if you've got a real &lt;code&gt;ls -l&lt;/code&gt; line or a bare permission string sitting in a hosting panel somewhere — parses it straight back into all three views so you can see exactly what it means before you touch it.&lt;/p&gt;

&lt;p&gt;It runs entirely in your browser: nothing you type or paste is sent anywhere.&lt;/p&gt;

&lt;p&gt;If permissions were the errand that brought you here, a few of Skojio's other free browser-based tools are worth a look too: the &lt;a href="https://dev.to/tools/json-formatter"&gt;JSON Formatter&lt;/a&gt; for tidying up config and API output, the &lt;a href="https://dev.to/tools/base64"&gt;Base64 encoder/decoder&lt;/a&gt; for the other everyday encoding puzzle, and the &lt;a href="https://dev.to/tools/password-generator"&gt;Password Generator&lt;/a&gt; for the account-security half of "locking things down properly."&lt;/p&gt;

</description>
      <category>linux</category>
      <category>permissions</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>How to decode a Base64 string (4 reliable ways)</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Wed, 01 Jul 2026 13:19:06 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/how-to-decode-a-base64-string-4-reliable-ways-k1e</link>
      <guid>https://dev.to/skojiocommunity/how-to-decode-a-base64-string-4-reliable-ways-k1e</guid>
      <description>&lt;p&gt;You copied a blob of letters and numbers out of a config file, a JWT, or an API response, and you need to know what it actually says. It looks like gibberish — &lt;code&gt;SGVsbG8sIHdvcmxkIQ==&lt;/code&gt; — but the trailing &lt;code&gt;==&lt;/code&gt; gives it away: that's Base64, and it decodes back to plain text in seconds.&lt;/p&gt;

&lt;p&gt;There are four reliable ways to decode it, depending on where you are and how awkward the string is. If you'd rather skip the command-line flags entirely, the &lt;a href="https://dev.to/tools/base64"&gt;Skojio Base64 decoder&lt;/a&gt; turns any string back into text or a downloadable file in one paste. Here's each method, and what to do when a string refuses to decode.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Base64 string actually is (and why it's not encryption)
&lt;/h2&gt;

&lt;p&gt;Base64 is a binary-to-text encoding. It maps arbitrary bytes onto 64 printable ASCII characters — &lt;code&gt;A–Z&lt;/code&gt;, &lt;code&gt;a–z&lt;/code&gt;, &lt;code&gt;0–9&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, and &lt;code&gt;/&lt;/code&gt; — so that binary data survives transport through systems that only accept text, like email headers, JSON fields, and URLs.&lt;/p&gt;

&lt;p&gt;Every 3 bytes of input become 4 characters of output, which is why Base64 is always about 33% larger than the source. The &lt;code&gt;=&lt;/code&gt; signs at the end are &lt;strong&gt;padding&lt;/strong&gt;: they pad the final group up to a multiple of four characters when the input length doesn't divide evenly by three.&lt;/p&gt;

&lt;p&gt;
  src="/blog/figures/base64-byte-mapping.jpg"&lt;br&gt;
  alt="The text 'Man' shown as three bytes, regrouped into four 6-bit chunks, and mapped to the Base64 characters T, W, F and u"&lt;br&gt;
  caption="Three bytes of text (24 bits) regroup into four 6-bit chunks — one Base64 character each."&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;The critical point: Base64 has no key. It is trivially reversible by anyone, so it provides &lt;strong&gt;zero&lt;/strong&gt; secrecy. A Base64-encoded password is a plaintext password with extra steps. Decoding it isn't decryption — it's just reading.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Because a Base64 string is plaintext, pasting one into a random online decoder hands your data to that server. If the blob is a JWT, an API response, or anything you'd treat as a credential, decode it locally — in the terminal, the browser console, or a client-side tool that never sends it over the network.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;You'll meet Base64 most often in four places: &lt;strong&gt;data URIs&lt;/strong&gt; (&lt;code&gt;data:image/png;base64,…&lt;/code&gt; that inline an image straight into CSS or HTML), &lt;strong&gt;JWTs&lt;/strong&gt; (whose header and payload are Base64url-encoded JSON), &lt;strong&gt;HTTP Basic Auth&lt;/strong&gt; (the &lt;code&gt;Authorization: Basic&lt;/code&gt; header is just &lt;code&gt;base64(user:password)&lt;/code&gt;), and &lt;strong&gt;email attachments&lt;/strong&gt; (MIME wraps binary parts in Base64). Recognising which one you're holding tells you what the decoded output should look like before you even run the command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decode a Base64 string in the terminal with &lt;code&gt;base64 --decode&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The fastest method on macOS and Linux is the built-in &lt;code&gt;base64&lt;/code&gt; command:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SGVsbG8sIHdvcmxkIQ=="&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;--decode&lt;/span&gt;
&lt;span class="c"&gt;# → Hello, world!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
  src="/blog/figures/base64-terminal.jpg"&lt;br&gt;
  alt="A terminal showing 'echo SGVsbG8sIHdvcmxkIQ== | base64 --decode' returning 'Hello, world!', and the reverse encode command"&lt;br&gt;
  caption="Decoding and re-encoding 'Hello, world!' with the built-in base64 command."&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;On older macOS the flag is capitalised:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SGVsbG8sIHdvcmxkIQ=="&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the decoded output is binary (an image, a gzip blob), don't print it to the terminal — redirect it to a file instead:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$B64&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;--decode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; output.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One terminal caveat: some PEM certificates and email payloads wrap Base64 at 64 characters per line. GNU &lt;code&gt;base64&lt;/code&gt; tolerates those newlines, but the BSD build on macOS can complain — pass &lt;code&gt;-i&lt;/code&gt; to ignore non-alphabet characters, or strip the newlines with &lt;code&gt;tr -d '\n'&lt;/code&gt; before piping.&lt;/p&gt;

&lt;p&gt;On Windows, &lt;code&gt;certutil&lt;/code&gt; or PowerShell do the same job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Text.Encoding&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;UTF8.GetString&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;FromBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SGVsbG8="&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;
  
  
  Decode Base64 in the browser console with &lt;code&gt;atob&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Already in DevTools? The browser ships a decoder: &lt;code&gt;atob&lt;/code&gt; (ASCII-to-binary).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SGVsbG8sIHdvcmxkIQ==&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → "Hello, world!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's one sharp edge. &lt;code&gt;atob&lt;/code&gt; returns a binary string, so any &lt;strong&gt;non-ASCII&lt;/strong&gt; content — accented characters, emoji, anything UTF-8 — comes out mangled. The correct incantation for Unicode is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find yourself reaching for that snippet more than once, paste the string into the &lt;a href="https://dev.to/tools/base64"&gt;Base64 tool&lt;/a&gt; instead — it handles UTF-8 correctly by default, so &lt;code&gt;café&lt;/code&gt; and &lt;code&gt;🚀&lt;/code&gt; round-trip without the &lt;code&gt;TextDecoder&lt;/code&gt; dance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your Base64 string won't decode — padding, URL-safe, and UTF-8
&lt;/h2&gt;

&lt;p&gt;Most "invalid Base64" errors come down to three causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Missing padding.&lt;/strong&gt; A valid Base64 string's length is a multiple of 4. If yours isn't, strict decoders reject it. Append &lt;code&gt;=&lt;/code&gt; until the length divides by four — one or two signs is all it ever needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL-safe variant.&lt;/strong&gt; Base64url, used in JWTs and URL query strings, swaps &lt;code&gt;+&lt;/code&gt; for &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; for &lt;code&gt;_&lt;/code&gt;, and usually drops padding entirely. A standard decoder chokes on it. Convert &lt;code&gt;-&lt;/code&gt; back to &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; back to &lt;code&gt;/&lt;/code&gt; first, then re-pad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stray whitespace.&lt;/strong&gt; Strings copied from emails, certificates, or wrapped JSON often carry newlines or spaces. Strip them before decoding — most command-line tools fail on embedded whitespace even though the data is fine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A JWT is the classic trap: it's three Base64url segments joined by dots. Split on the &lt;code&gt;.&lt;/code&gt;, decode the first two segments, and you get the header and payload as JSON. The signature won't decode to anything readable — that's expected.&lt;/p&gt;

&lt;p&gt;Data URIs hide a fourth gotcha. A string like &lt;code&gt;data:image/png;base64,iVBORw0KGgo…&lt;/code&gt; isn't pure Base64 — the part you decode starts &lt;strong&gt;after&lt;/strong&gt; the comma. Feed the whole &lt;code&gt;data:&lt;/code&gt; prefix into a decoder and it fails; strip everything up to and including the comma first, then decode the remainder to recover the original file bytes.&lt;/p&gt;

&lt;p&gt;If you only need to verify that a string &lt;em&gt;is&lt;/em&gt; valid Base64 without caring about the output, check two things: the alphabet (only &lt;code&gt;A–Z&lt;/code&gt;, &lt;code&gt;a–z&lt;/code&gt;, &lt;code&gt;0–9&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, and trailing &lt;code&gt;=&lt;/code&gt; for standard Base64) and the length-multiple-of-4 rule. Anything outside that alphabet means it's either URL-safe, corrupted, or not Base64 at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decode any Base64 string in one paste
&lt;/h2&gt;

&lt;p&gt;When the variant is ambiguous, the padding is missing, or the result is a file rather than text, stop fiddling with flags and paste it into a decoder that just works.&lt;/p&gt;



&lt;p&gt;The &lt;a href="https://dev.to/tools/base64"&gt;Skojio Base64 encoder and decoder&lt;/a&gt; auto-detects standard and URL-safe input, fixes padding for you, decodes UTF-8 correctly, and lets you decode straight to a downloadable file for binary payloads. It runs entirely in your browser, so a JWT or an internal API token never leaves your machine — no upload, no account, no rate limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where you are&lt;/th&gt;
&lt;th&gt;How to decode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS / Linux terminal&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;base64 --decode&lt;/code&gt; (or &lt;code&gt;-D&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;certutil -decode&lt;/code&gt; or &lt;code&gt;FromBase64String&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser DevTools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;atob()&lt;/code&gt; — wrap in &lt;code&gt;TextDecoder&lt;/code&gt; for UTF-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Awkward string, URL-safe, or a file&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/tools/base64"&gt;Skojio Base64 decoder&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Base64 is an encoding, not a secret. Once you can read it on demand, the blob in your config file stops being mysterious — and when the next one resists &lt;code&gt;base64 --decode&lt;/code&gt;, &lt;a href="https://dev.to/tools/base64"&gt;paste it into the decoder&lt;/a&gt; and let it sort the padding out for you.&lt;/p&gt;

</description>
      <category>base64</category>
      <category>encoding</category>
      <category>developertips</category>
    </item>
    <item>
      <title>What does a cron expression mean? Read it field by field</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Wed, 01 Jul 2026 13:19:05 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/what-does-a-cron-expression-mean-read-it-field-by-field-1cmd</link>
      <guid>https://dev.to/skojiocommunity/what-does-a-cron-expression-mean-read-it-field-by-field-1cmd</guid>
      <description>&lt;p&gt;Someone hands you &lt;code&gt;0 */2 * * 1-5&lt;/code&gt; and asks "when does that run?" You can read the five fields, but the slash and the dash turn it into a small puzzle. Cron syntax is compact by design — five fields and four symbols cover almost every schedule you'll ever need — but compact means easy to misread.&lt;/p&gt;

&lt;p&gt;The good news: a cron expression is fully decodable once you know what each field and symbol does. This guide reads one field by field. If you'd rather just paste an expression and see the answer in plain English, the &lt;a href="https://dev.to/tools/cron-expression-helper"&gt;Skojio cron expression helper&lt;/a&gt; translates it and lists the next 10 fire times instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five fields of a cron expression, left to right
&lt;/h2&gt;

&lt;p&gt;A standard cron line is five space-separated fields followed by the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;┌───────── &lt;span class="n"&gt;minute&lt;/span&gt;        (&lt;span class="m"&gt;0&lt;/span&gt;–&lt;span class="m"&gt;59&lt;/span&gt;)
│ ┌─────── &lt;span class="n"&gt;hour&lt;/span&gt;          (&lt;span class="m"&gt;0&lt;/span&gt;–&lt;span class="m"&gt;23&lt;/span&gt;)
│ │ ┌───── &lt;span class="n"&gt;day&lt;/span&gt;-&lt;span class="n"&gt;of&lt;/span&gt;-&lt;span class="n"&gt;month&lt;/span&gt;  (&lt;span class="m"&gt;1&lt;/span&gt;–&lt;span class="m"&gt;31&lt;/span&gt;)
│ │ │ ┌─── &lt;span class="n"&gt;month&lt;/span&gt;         (&lt;span class="m"&gt;1&lt;/span&gt;–&lt;span class="m"&gt;12&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;JAN&lt;/span&gt;–&lt;span class="n"&gt;DEC&lt;/span&gt;)
│ │ │ │ ┌─ &lt;span class="n"&gt;day&lt;/span&gt;-&lt;span class="n"&gt;of&lt;/span&gt;-&lt;span class="n"&gt;week&lt;/span&gt;   (&lt;span class="m"&gt;0&lt;/span&gt;–&lt;span class="m"&gt;6&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;SUN&lt;/span&gt;–&lt;span class="n"&gt;SAT&lt;/span&gt;; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;both&lt;/span&gt; &lt;span class="n"&gt;Sunday&lt;/span&gt;)
│ │ │ │ │
&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt; * * &lt;span class="m"&gt;1&lt;/span&gt;   /&lt;span class="n"&gt;path&lt;/span&gt;/&lt;span class="n"&gt;to&lt;/span&gt;/&lt;span class="n"&gt;job&lt;/span&gt;.&lt;span class="n"&gt;sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read positionally, never by name — cron doesn't label its fields, so &lt;code&gt;9&lt;/code&gt; in the second slot is the hour, full stop. The example above means &lt;strong&gt;09:00, on Mondays&lt;/strong&gt;. Everything else is built from how each field is filled.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Count the fields first. Five means standard cron; six usually means a seconds field is bolted on the front (common in Quartz and some app schedulers). Misjudging which dialect you're in is the fastest way to read every field one position out.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;,&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, and &lt;code&gt;/&lt;/code&gt; in any cron field
&lt;/h2&gt;

&lt;p&gt;Four symbols do all the work, and they mean the same thing in every field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;*&lt;/code&gt; (asterisk)&lt;/strong&gt; — every valid value. &lt;code&gt;*&lt;/code&gt; in the hour field means every hour. &lt;code&gt;* * * * *&lt;/code&gt; runs once a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;,&lt;/code&gt; (comma)&lt;/strong&gt; — a list. &lt;code&gt;0,30&lt;/code&gt; in minutes means "at 0 and at 30". &lt;code&gt;1,15&lt;/code&gt; in day-of-month means the 1st and the 15th.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-&lt;/code&gt; (dash)&lt;/strong&gt; — an inclusive range. &lt;code&gt;1-5&lt;/code&gt; in day-of-week means Monday through Friday. &lt;code&gt;9-17&lt;/code&gt; in hours means every hour from 9am to 5pm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/&lt;/code&gt; (slash)&lt;/strong&gt; — a step. &lt;code&gt;*/15&lt;/code&gt; in minutes means every 15th minute (0, 15, 30, 45). You can combine it with a range: &lt;code&gt;0-30/10&lt;/code&gt; means minutes 0, 10, 20, 30.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The slash trips people up most. &lt;code&gt;*/15&lt;/code&gt; is &lt;strong&gt;fixed clock positions&lt;/strong&gt;, not an offset from when you saved the crontab — if you save at 14:07, the next run is 14:15, not 14:22. The same logic governs the &lt;a href="https://dev.to/blog/three-cron-mistakes-that-break-overnight-jobs"&gt;three cron mistakes that break overnight jobs&lt;/a&gt;, so it's worth internalising early.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;0 9 * * 1-5&lt;/code&gt; means in plain English (worked example)
&lt;/h2&gt;

&lt;p&gt;Take it field by field:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Reads as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;minute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;at minute 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hour&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;of the 9th hour (09:00)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;day-of-month&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every day of the month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;month&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;day-of-week&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Monday to Friday&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Put together: &lt;strong&gt;09:00 every weekday&lt;/strong&gt;. A classic "send the morning report on business days" schedule.&lt;/p&gt;

&lt;p&gt;
  src="/blog/figures/cron-anatomy.jpg"&lt;br&gt;
  alt="The cron expression 0 9 * * 1-5 broken into five labelled fields — minute, hour, day, month and weekday — reading as 09:00 Monday to Friday"&lt;br&gt;
  caption="Reading 0 9 * * 1-5 field by field: 09:00, Monday to Friday."&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;Now read &lt;code&gt;0 */2 * * 1-5&lt;/code&gt; the same way: minute 0, every 2nd hour (00:00, 02:00, 04:00 … 22:00), every weekday — so on the hour, every two hours, Monday to Friday. Reading positionally and resolving each symbol turns any expression into a sentence.&lt;/p&gt;

&lt;p&gt;
  src="/blog/figures/cron-explain-terminal.jpg"&lt;br&gt;
  alt="A terminal translating the cron expression 0 */2 * * 1-5 into plain English: at minute 0, every 2nd hour, Monday through Friday"&lt;br&gt;
  caption="The same reading, automated: paste an expression, get a sentence and the next run times."&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;One more that looks harmless but isn't: &lt;code&gt;0 0 1,15 * *&lt;/code&gt; means midnight on the &lt;strong&gt;1st and 15th&lt;/strong&gt; of every month — a list, not a range. Swap the comma for a dash and &lt;code&gt;0 0 1-15 * *&lt;/code&gt; becomes midnight on &lt;strong&gt;every day from the 1st to the 15th&lt;/strong&gt;, which is fifteen runs a month instead of two. The single character is the entire difference, which is exactly why reading each symbol deliberately beats skimming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Special strings and the optional sixth field
&lt;/h2&gt;

&lt;p&gt;Many cron implementations accept named shortcuts that replace all five fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@hourly&lt;/code&gt; = &lt;code&gt;0 * * * *&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@daily&lt;/code&gt; (or &lt;code&gt;@midnight&lt;/code&gt;) = &lt;code&gt;0 0 * * *&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@weekly&lt;/code&gt; = &lt;code&gt;0 0 * * 0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@monthly&lt;/code&gt; = &lt;code&gt;0 0 1 * *&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@reboot&lt;/code&gt; = once, at daemon startup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll also see &lt;strong&gt;six-field&lt;/strong&gt; expressions in the wild. Some schedulers (Quartz, Spring, many container cron images) prepend a &lt;strong&gt;seconds&lt;/strong&gt; field, so &lt;code&gt;*/30 * * * * *&lt;/code&gt; means "every 30 seconds". If an expression has six fields and the first looks like a seconds step, that's why — count the fields before you read them, because a five-field parser will misread a six-field line entirely.&lt;/p&gt;

&lt;p&gt;A couple of field-specific quirks are worth knowing too. Months and weekdays accept &lt;strong&gt;three-letter names&lt;/strong&gt;: &lt;code&gt;0 0 * JAN MON&lt;/code&gt; is as valid as &lt;code&gt;0 0 * 1 1&lt;/code&gt;, and often clearer. Day-of-week is the field most likely to bite, because &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;7&lt;/code&gt; both mean Sunday — so a range like &lt;code&gt;0-6&lt;/code&gt; and &lt;code&gt;1-7&lt;/code&gt; both cover the full week, just with the boundary in a different place. And &lt;code&gt;?&lt;/code&gt; shows up in Quartz-style expressions as a "no specific value" marker in the two day fields, used to sidestep the day-of-month-versus-day-of-week ambiguity that standard cron handles as an OR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read any cron expression in plain English
&lt;/h2&gt;

&lt;p&gt;Reading by hand is a good skill, but the safest check before you ship a schedule is to confirm the next fire times match your intent.&lt;/p&gt;



&lt;p&gt;The &lt;a href="https://dev.to/tools/cron-expression-helper"&gt;cron expression helper&lt;/a&gt; parses an expression, describes it in plain English, and lists the next 10 runs in your chosen timezone — which also catches the day-of-month-versus-day-of-week surprise that pure field-reading can miss. Because schedules drift across regions, pair it with the &lt;a href="https://dev.to/tools/timezone-helper"&gt;Skojio timezone helper&lt;/a&gt; when a job has to fire at a specific local time. Both run in your browser with no install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every value&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;* * * * *&lt;/code&gt; → every minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;,&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0,30&lt;/code&gt; → at :00 and :30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inclusive range&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1-5&lt;/code&gt; → Mon–Fri&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;step (fixed positions)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;*/15&lt;/code&gt; → :00 :15 :30 :45&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five fields, four symbols, read left to right — that's the whole grammar. Once it's second nature you can decode most expressions at a glance, and when one looks ambiguous, &lt;a href="https://dev.to/tools/cron-expression-helper"&gt;drop it into the cron helper&lt;/a&gt; and let the next fire times confirm you read it right.&lt;/p&gt;

</description>
      <category>cron</category>
      <category>devops</category>
      <category>developertips</category>
    </item>
    <item>
      <title>How to remove unused CSS and shrink your stylesheet</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Wed, 01 Jul 2026 13:18:34 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/how-to-remove-unused-css-and-shrink-your-stylesheet-15m8</link>
      <guid>https://dev.to/skojiocommunity/how-to-remove-unused-css-and-shrink-your-stylesheet-15m8</guid>
      <description>&lt;p&gt;A fresh Bootstrap build is roughly 200KB of CSS. A typical site uses maybe a tenth of it. The other 90% ships to every visitor, parses on every page load, and lingers in your bundle forever — styling components you deleted two redesigns ago.&lt;/p&gt;

&lt;p&gt;Unused CSS is the quiet tax on your Core Web Vitals: bytes downloaded, parsed, and matched against the DOM for no reason. The good news is that most of it is findable in the browser you already have open, and removable without rewriting a line of markup. If you'd rather skip the diagnosis and just shrink the file you ship, &lt;a href="https://dev.to/tools/css-cleaner"&gt;the Skojio CSS cleaner&lt;/a&gt; strips comments, duplicate properties, and whitespace from any stylesheet in one paste — no upload, no build step.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Unused CSS comes in two forms: dead selectors (whole rules nothing matches) and redundant bytes inside the rules you keep.&lt;/li&gt;
&lt;li&gt;The Chrome DevTools Coverage panel shows exactly how much of each stylesheet goes unused on a given page.&lt;/li&gt;
&lt;li&gt;PurgeCSS removes dead selectors during a build — but it will strip classes your JavaScript adds at runtime unless you safelist them.&lt;/li&gt;
&lt;li&gt;Minifying never deletes a rule; it only removes the bytes around the rules you keep.&lt;/li&gt;
&lt;li&gt;You can shrink the stylesheet you ship — comments, duplicate properties, whitespace — entirely in your browser, no pipeline required.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  Why unused CSS piles up in production
&lt;/h2&gt;

&lt;p&gt;CSS only ever grows. Every framework you pull in, every component you ship, every A/B test you forget to clean up adds rules — and nothing in the normal workflow ever takes them back out.&lt;/p&gt;

&lt;p&gt;Three habits do most of the damage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frameworks shipped whole.&lt;/strong&gt; Bootstrap, Bulma, or an old Tailwind build without a configured &lt;code&gt;content&lt;/code&gt; array all bundle thousands of utility classes you will never reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Components that outlive their markup.&lt;/strong&gt; You delete the promo-banner component but leave its 40 lines of CSS behind, because nothing breaks when you don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy-pasted resets and vendor blocks.&lt;/strong&gt; The same normalise rules, pasted into three stylesheets, then concatenated and minified together into one fat file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It helps to split the waste into two kinds, because they have completely different fixes. One is a &lt;em&gt;diagnosis&lt;/em&gt; problem — you have to work out which rules nothing uses. The other is a &lt;em&gt;cleanup&lt;/em&gt; problem — the rules you keep are simply written with more bytes than they need.&lt;/p&gt;

&lt;p&gt;
  src="/blog/figures/css-two-kinds-of-waste.jpg"&lt;br&gt;
  alt="A diagram splitting CSS waste into two rows: dead selectors, fixed with DevTools Coverage and PurgeCSS, and redundant bytes such as comments and duplicate properties, fixed by minifying"&lt;br&gt;
  caption="Dead selectors need a diagnosis tool; redundant bytes just need cleaning — and only the first needs a build step."&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;Get the categories straight and the rest of the job is mechanical. Mixing them up is how people either ship a bloated file they think is clean, or delete a rule that turns out to matter.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to find unused CSS in the Chrome DevTools Coverage panel
&lt;/h2&gt;

&lt;p&gt;The browser ships the diagnosis tool. In Chrome or Edge, open DevTools, press &lt;strong&gt;Cmd+Shift+P&lt;/strong&gt; (Ctrl+Shift+P on Windows) and run &lt;strong&gt;Show Coverage&lt;/strong&gt;. Click the reload icon in the Coverage tab and interact with the page while it records.&lt;/p&gt;

&lt;p&gt;Each stylesheet then gets a red-and-green bar. Green is bytes that matched something; red is bytes that never did. Click a file and the source view highlights the unused lines directly, so you can see &lt;em&gt;which&lt;/em&gt; selectors are dead rather than just a percentage.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Coverage is per-page and per-interaction. It only marks a rule "used" if you actually triggered it — so open the modal, hover the dropdown, and visit the checkout before you trust the numbers. A rule that looks dead on the homepage may be the only thing styling a page you didn't click.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;This is the right tool for &lt;em&gt;measuring&lt;/em&gt; the problem, not fixing it. Coverage won't rewrite your files, and deleting by hand from a 5,000-line stylesheet is how regressions happen. Treat the panel as the audit that tells you whether the waste is worth chasing — if a stylesheet is 80% red, it is. Once you've confirmed there are real dead rules to remove, automate the removal rather than hand-editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remove unused CSS in a build with PurgeCSS (and where it falls short)
&lt;/h2&gt;

&lt;p&gt;The standard way to delete dead selectors automatically is &lt;strong&gt;PurgeCSS&lt;/strong&gt;. It scans your markup and scripts for the class names and IDs you actually reference, then drops every rule whose selector never appears.&lt;/p&gt;

&lt;p&gt;
  src="/blog/figures/css-purge-terminal.jpg"&lt;br&gt;
  alt="A terminal running purgecss against a 248KB stylesheet, which prints a new 31KB file after removing the dead rules"&lt;br&gt;
  caption="PurgeCSS scans your content for referenced selectors and rewrites the stylesheet without the dead rules."&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;Run it against your built output and a Bootstrap-sized file routinely drops by 80–90%. Tailwind does the same thing natively now — its &lt;code&gt;content&lt;/code&gt; array is a built-in purge step — which is why a modern Tailwind build is small even though the framework defines millions of possible utilities.&lt;/p&gt;

&lt;p&gt;The catch is everything PurgeCSS can't &lt;em&gt;see&lt;/em&gt;. It matches selectors as literal strings in your source, so any class name your code builds at runtime is invisible to it.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
PurgeCSS strips classes added by JavaScript unless you safelist them. A line like &lt;code&gt;el.classList.add('is-' + state)&lt;/code&gt; produces class names that appear nowhere in your source as literal text, so the matching rules get purged and the component renders unstyled in production — while looking perfect in dev, where you didn't run the purge. Add a &lt;code&gt;safelist&lt;/code&gt; for any dynamically composed class before you trust the output.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;So PurgeCSS handles dead selectors well, with a safelist as the price of admission. What it deliberately does &lt;em&gt;not&lt;/em&gt; touch is the second kind of waste — the bytes inside the rules you're keeping.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Dead selectors are a diagnosis problem; redundant bytes are a cleanup problem — and only one of them needs a build step.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Shrink the CSS you keep — without a build pipeline
&lt;/h2&gt;

&lt;p&gt;After the dead rules are gone, the file you ship is still padded with bytes that do nothing: developer comments, blank lines, four-space indentation, and duplicate declarations where the same property is set twice in one block. Minifying removes all of it without changing a single thing the browser renders.&lt;/p&gt;

&lt;p&gt;You don't need a bundler to do this for a one-off stylesheet, a vendor file, or a snippet a designer handed you. Paste it into a client-side cleaner and get the minified version straight back. &lt;a href="https://dev.to/tools/css-cleaner"&gt;Skojio's CSS cleaner&lt;/a&gt; strips comments, collapses whitespace, removes duplicate properties within a block, and can sort declarations for a consistent diff — all in the browser, so the stylesheet never leaves your machine.&lt;/p&gt;



&lt;p&gt;It won't hunt down dead selectors — that's the Coverage-and-PurgeCSS job above, because only your markup knows which rules are truly unused. But for squeezing the redundancy out of the CSS you've decided to keep, &lt;a href="https://dev.to/tools/css-cleaner"&gt;pasting the file into the cleaner&lt;/a&gt; is faster than wiring up a build for a file you'll process once, and it typically shaves 30–70% off the size before compression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind of waste&lt;/th&gt;
&lt;th&gt;How to remove it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dead selectors (nothing matches)&lt;/td&gt;
&lt;td&gt;Find with DevTools Coverage, remove with PurgeCSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Classes added by JavaScript at runtime&lt;/td&gt;
&lt;td&gt;Safelist them before purging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comments, whitespace, duplicate properties&lt;/td&gt;
&lt;td&gt;Minify and clean the stylesheet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A one-off file with no build set up&lt;/td&gt;
&lt;td&gt;Paste into a client-side CSS cleaner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Find the dead rules in Coverage, prune them in your build, then &lt;a href="https://dev.to/tools/css-cleaner"&gt;run the file through the cleaner&lt;/a&gt; to squeeze out the bytes that are left. Most stylesheets come out a third smaller before you've touched a line of markup — and your visitors stop downloading two redesigns' worth of CSS they'll never see.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webperf</category>
      <category>developertips</category>
    </item>
    <item>
      <title>Schedule a meeting across time zones without the guesswork</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Wed, 01 Jul 2026 13:18:33 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/schedule-a-meeting-across-time-zones-without-the-guesswork-al8</link>
      <guid>https://dev.to/skojiocommunity/schedule-a-meeting-across-time-zones-without-the-guesswork-al8</guid>
      <description>&lt;p&gt;"Let's do 3pm." Whose 3pm? Yours, the colleague in San Francisco who is still asleep, or the client in Sydney for whom it is already tomorrow? A surprising share of the calendar invites that land an hour wrong start with exactly that sentence.&lt;/p&gt;

&lt;p&gt;Coordinating a call across continents is a solved problem — you just need to see everyone's working hours at once instead of doing offset arithmetic in your head. If you'd rather skip the maths entirely, &lt;a href="https://dev.to/tools/timezone-helper"&gt;the Skojio timezone helper&lt;/a&gt; stacks every participant's zone on one 24-hour timeline, marks where the working days overlap, and hands you a link to share the exact slot.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Most scheduling errors come from doing UTC-offset arithmetic by hand — and from forgetting that offsets change with daylight saving.&lt;/li&gt;
&lt;li&gt;The fastest way to pick a slot is visual: stack everyone's working hours on one timeline and look for the overlap.&lt;/li&gt;
&lt;li&gt;Communicate times in UTC, written as ISO 8601 with an offset, so there is never a "your time or mine?".&lt;/li&gt;
&lt;li&gt;Pin recurring meetings to a named zone like Europe/London, not a fixed UTC time, so they follow daylight-saving shifts.&lt;/li&gt;
&lt;li&gt;Time-zone drift breaks scheduled jobs the same way it breaks meetings.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  Why scheduling across time zones goes wrong
&lt;/h2&gt;

&lt;p&gt;The trap is assuming the gap between two cities is a fixed number. It is not. London and New York are five hours apart for most of the year — but for roughly two weeks each spring and autumn, when one region has changed its clocks and the other has not, the gap is four hours. Every "always five hours apart" rule of thumb is wrong twice a year.&lt;/p&gt;

&lt;p&gt;Three things compound it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bare local times.&lt;/strong&gt; "3pm" carries no zone. The moment it leaves your calendar and lands in an email, half the recipients silently assume it means their 3pm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The date line.&lt;/strong&gt; When it is 9am Monday in California, it is already 2am Tuesday in Sydney. Mental arithmetic that ignores the day rollover books people for the wrong date, not just the wrong hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wrong canonical reference.&lt;/strong&gt; Teams store "the meeting time" as whatever the organiser's clock said, so when the organiser travels or their region shifts, the stored time moves with them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get those three straight and the rest is mechanical. Ignore them and you generate a steady trickle of missed calls that everyone blames on "time zones" rather than on arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find a meeting time that works in every time zone
&lt;/h2&gt;

&lt;p&gt;Stop converting in your head. The reliable method is visual: take each participant's working hours — say 09:00 to 17:00 local — translate them onto one shared axis, and look for the column where the bands overlap.&lt;/p&gt;

&lt;p&gt;Worked example. A call between London, New York, and São Paulo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;London (UTC+1 in summer): 09:00–17:00 local is &lt;strong&gt;08:00–16:00 UTC&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;New York (UTC−4): 09:00–17:00 local is &lt;strong&gt;13:00–21:00 UTC&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;São Paulo (UTC−3): 09:00–17:00 local is &lt;strong&gt;12:00–20:00 UTC&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only window all three share is &lt;strong&gt;13:00–16:00 UTC&lt;/strong&gt; — early afternoon in London, mid-morning in New York and São Paulo. Any slot in that band is inside everyone's working day.&lt;/p&gt;

&lt;p&gt;
  src="/blog/figures/timezones-overlap.jpg"&lt;br&gt;
  alt="Three stacked 24-hour timelines for London, New York and São Paulo with each city's nine-to-five band shaded, and the 13:00 to 16:00 UTC overlap highlighted"&lt;br&gt;
  caption="Stack each city's working hours on one UTC axis and the shared window is obvious — here, 13:00–16:00 UTC."&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;Rather than work that out by hand, &lt;a href="https://dev.to/tools/timezone-helper"&gt;drop each city onto the timeline&lt;/a&gt; and read the overlap straight off the grid. When the zones are far enough apart that no band overlaps — San Francisco and Sydney, classically — there is no good answer, only a least-bad one. Seeing that on screen is the point: it tells you to rotate the awkward slot between teams instead of always burdening the same people.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Anchor on whoever has the narrowest availability. If one person observes a school run or a hard 18:00 stop, their constraint defines the window — fit everyone else around it rather than finding a slot that suits the majority and quietly excludes them.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Store and share times in UTC and ISO 8601
&lt;/h2&gt;

&lt;p&gt;Pick one canonical reference for every stored timestamp, and make it &lt;strong&gt;UTC&lt;/strong&gt;. Convert to local only at the moment you display it. That single rule removes a whole category of bug, because the stored value never depends on where anyone happens to be sitting.&lt;/p&gt;

&lt;p&gt;When you write a time down for a human, use &lt;strong&gt;ISO 8601 with an explicit offset&lt;/strong&gt;: &lt;code&gt;2026-06-10T15:00:00+01:00&lt;/code&gt;. It is unambiguous, sorts correctly as text, and every calendar app parses it.&lt;/p&gt;

&lt;p&gt;
  src="/blog/figures/timezones-date-terminal.jpg"&lt;br&gt;
  alt="A terminal rendering one UTC instant, 14:00 UTC, in London, New York and Sydney using the date command with the TZ variable, showing 15:00 BST, 10:00 EDT and midnight the next day in Sydney"&lt;br&gt;
  caption="One instant in UTC, rendered in three zones — note the DST-correct abbreviations and Sydney's date rollover."&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;For sharing, the rule is the same: never write a bare "3pm" and hope. Send the ISO timestamp, or a link that renders the slot in each person's local zone so nobody has to count. The same drift that derails meetings also derails automation — a scheduled task set in the wrong zone fires at the wrong hour — which is exactly why the &lt;a href="https://dev.to/tools/cron-expression-helper"&gt;cron expression helper&lt;/a&gt; shows fire times in a zone you choose rather than guessing the server's.&lt;/p&gt;
&lt;h2&gt;
  
  
  Daylight saving: the shift that breaks recurring calls
&lt;/h2&gt;

&lt;p&gt;Daylight saving is where "I'll just remember the offset" finally collapses. Regions switch on different weekends, the southern hemisphere runs opposite to the northern, and places like most of Arizona or the entire UTC-anchored server fleet never switch at all. The offset between any two of them is a moving target.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
An offset is a fact about a moment, not a property of a city — and it changes twice a year.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;The costliest version is the recurring meeting. Book a weekly call as a fixed UTC time and it will drift by an hour for half the attendees the next time a clock changes — they show up early or late for weeks until someone notices.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
Pin recurring events to a named IANA zone (Europe/London, America/New_York), never to a fixed UTC offset. A calendar that knows the zone applies each daylight-saving transition automatically; one that only knows "+01:00" freezes that offset and silently shifts the call for everyone the moment the rules change.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare any time zones in one screen
&lt;/h2&gt;

&lt;p&gt;When a call spans three or more zones, stop converting in your head and put them all on one timeline.&lt;/p&gt;



&lt;p&gt;&lt;a href="https://dev.to/tools/timezone-helper"&gt;Skojio's timezone helper&lt;/a&gt; lays every zone on a single 24-hour grid, stays accurate through daylight saving by using live zone data, lets you drag the time to find a slot, and gives you a shareable link that opens in each colleague's own local time. It runs entirely in your browser — no account, no calendar permissions, nothing uploaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguous "3pm"&lt;/td&gt;
&lt;td&gt;Attach a zone, or send ISO 8601 with the offset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offset arithmetic by hand&lt;/td&gt;
&lt;td&gt;Stack zones on one timeline and read the overlap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recurring call drifts at DST&lt;/td&gt;
&lt;td&gt;Pin the event to a named zone, not a fixed UTC offset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No shared working hours&lt;/td&gt;
&lt;td&gt;Rotate the awkward slot; share a link so no one miscounts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Time zones are only hard when you do them in your head. &lt;a href="https://dev.to/tools/timezone-helper"&gt;Line the zones up&lt;/a&gt; on one screen, agree the slot in UTC, and share it as a link — the next "whose 3pm?" thread never gets started.&lt;/p&gt;

</description>
      <category>timezones</category>
      <category>productivity</category>
      <category>developertips</category>
    </item>
  </channel>
</rss>
