<?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>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>
    <item>
      <title>Three cron mistakes that quietly break overnight jobs</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Thu, 28 May 2026 22:43:25 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/three-cron-mistakes-that-quietly-break-overnight-jobs-417d</link>
      <guid>https://dev.to/skojiocommunity/three-cron-mistakes-that-quietly-break-overnight-jobs-417d</guid>
      <description>&lt;p&gt;A cron expression is five fields. How wrong can it go? Quite wrong, as it turns out — and the failure mode is usually silent. The job runs, it just runs at the wrong time, or sixty times in a row, or never on the day you actually need it.&lt;/p&gt;

&lt;p&gt;Here are the three patterns I've watched break production overnight, and how to spot them before they bite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1 — Treating &lt;code&gt;*/5&lt;/code&gt; as "every 5 from now"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;*/5 * * * *&lt;/code&gt; does not mean "run every 5 minutes starting from when you save this". It means "run at minutes 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 of every hour".&lt;/p&gt;

&lt;p&gt;If you save the cron at 14:03 expecting the first run at 14:08, you will be wrong. The first run will be at 14:05.&lt;/p&gt;

&lt;p&gt;Worse, &lt;code&gt;*/7 * * * *&lt;/code&gt; runs at minutes 0, 7, 14, 21, 28, 35, 42, 49, 56 — and then &lt;strong&gt;0 again 4 minutes later&lt;/strong&gt;, because the next hour resets the cycle. Anything that isn't a divisor of 60 will misbehave at the hour boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2 — Day-of-month AND day-of-week
&lt;/h2&gt;

&lt;p&gt;The fifth field (day of week) and the third field (day of month) have a non-obvious interaction. If &lt;strong&gt;either&lt;/strong&gt; is unrestricted (&lt;code&gt;*&lt;/code&gt;), cron treats the other as the active filter. If &lt;strong&gt;both&lt;/strong&gt; are specified, cron runs when &lt;strong&gt;either&lt;/strong&gt; matches — an OR, not an AND.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;0 9 1 * 1&lt;/code&gt; does NOT mean "9am on Mondays that fall on the 1st". It means "9am on the 1st of every month, AND 9am every Monday". That's roughly 8 runs per month instead of zero or one.&lt;/p&gt;

&lt;p&gt;If you want a true AND, you have to filter inside the script:&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="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +&lt;span class="se"&gt;\%&lt;/span&gt;u&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; /path/to/job.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use a cron-like scheduler that supports compound conditions natively (Quartz, for instance).&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 3 — Timezone drift
&lt;/h2&gt;

&lt;p&gt;Most cron daemons run in &lt;strong&gt;the system's local timezone&lt;/strong&gt;, which is often UTC on cloud VMs and often local time on developer machines.&lt;/p&gt;

&lt;p&gt;Two failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DST transitions.&lt;/strong&gt; A cron set to &lt;code&gt;0 2 * * *&lt;/code&gt; in a region that observes daylight saving will either skip a day (spring forward) or run twice (fall back) on transition nights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container drift.&lt;/strong&gt; A container's TZ defaults to UTC. If your developer laptop says BST and the production container says UTC, the same crontab runs at different wall-clock times.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is to always be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In crontab itself (Linux):&lt;/span&gt;
&lt;span class="nv"&gt;CRON_TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Europe/London
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /path/to/backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run everything in UTC and convert in the application layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer authoring loop
&lt;/h2&gt;

&lt;p&gt;Before you commit a cron expression to production, paste it into a parser that shows you the next 10 fire times. If the times match what you expected, ship it. If they don't, you've caught the mistake before it costs you a night.&lt;/p&gt;



&lt;p&gt;The Skojio cron helper does exactly that — paste an expression, see the next 10 fire times in your chosen timezone, plus a plain-English description of what the expression actually means. It catches all three of the mistakes above in seconds.&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;Mistake&lt;/th&gt;
&lt;th&gt;How to spot it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;*/N&lt;/code&gt; where N doesn't divide 60&lt;/td&gt;
&lt;td&gt;Hour-boundary glitch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day-of-month + day-of-week both set&lt;/td&gt;
&lt;td&gt;More runs than expected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implicit timezone&lt;/td&gt;
&lt;td&gt;DST or container drift&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cron is fine when you respect it. The above three failure modes account for nearly every "why did this run / not run" Slack thread I've seen.&lt;/p&gt;

</description>
      <category>cron</category>
      <category>devops</category>
      <category>developertips</category>
    </item>
    <item>
      <title>How to format JSON in Chrome without an extension</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Thu, 28 May 2026 22:43:24 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/how-to-format-json-in-chrome-without-an-extension-9pm</link>
      <guid>https://dev.to/skojiocommunity/how-to-format-json-in-chrome-without-an-extension-9pm</guid>
      <description>&lt;p&gt;Anyone who has stared at a 600-line JSON response from an API knows the moment: you want to scan it, but Chrome's default view is a single wall of text with no indentation, no folding, no hope.&lt;/p&gt;

&lt;p&gt;You can solve this without installing anything. Three browser-native tricks cover most cases, and one free Skojio tool covers the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trick 1 — Use the DevTools Network panel
&lt;/h2&gt;

&lt;p&gt;Open DevTools, switch to the Network tab, click the request that returned the JSON, then open the &lt;strong&gt;Response&lt;/strong&gt; sub-tab and click the small &lt;code&gt;{}&lt;/code&gt; pretty-print button at the bottom-left of the panel.&lt;/p&gt;

&lt;p&gt;Chrome reformats the response in place: indentation, line breaks, the lot. No extension required.&lt;/p&gt;

&lt;p&gt;The catch: this only works for responses Chrome captured during a network request. If you have JSON pasted from a Slack message or saved to a &lt;code&gt;.json&lt;/code&gt; file, this trick doesn't help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trick 2 — Paste it into the Console
&lt;/h2&gt;

&lt;p&gt;The DevTools Console gives you a real JavaScript REPL. Paste this in:&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;paste your JSON here&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome renders the parsed object as a collapsible tree. You can click to expand and collapse, search within keys with &lt;code&gt;Ctrl+F&lt;/code&gt;, and copy individual values cleanly.&lt;/p&gt;

&lt;p&gt;This works well for moderately sized payloads. It struggles when the JSON contains backticks (which break the template literal) or when the payload is large enough to make the console laggy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trick 3 — view-source: for static files
&lt;/h2&gt;

&lt;p&gt;If you have JSON saved as a file or hosted somewhere static, prefix the URL with &lt;code&gt;view-source:&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;view-source:https://api.example.com/data.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome renders it monospace with line numbers. Combined with &lt;code&gt;Ctrl+F&lt;/code&gt; this is enough to scan structure quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the tricks fall short
&lt;/h2&gt;

&lt;p&gt;The browser tricks fail when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to &lt;strong&gt;validate&lt;/strong&gt; the JSON, not just view it (catch missing commas, wrong quote types)&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;minify&lt;/strong&gt; for production (strip whitespace)&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;compare&lt;/strong&gt; two JSON payloads visually&lt;/li&gt;
&lt;li&gt;You're pasting JSON with embedded backticks or other awkward characters&lt;/li&gt;
&lt;li&gt;You want a permanent URL you can bookmark&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;The Skojio JSON Formatter runs entirely in your browser — no data leaves your machine — and handles all of the above in a single panel. It validates with line-number errors, lets you toggle between formatted and minified views, and you can paste anything (backticks included) without losing your mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the right tool for the moment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Best option&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API response in DevTools&lt;/td&gt;
&lt;td&gt;Pretty-print button in Network panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quick tree view of pasted JSON&lt;/td&gt;
&lt;td&gt;Console &lt;code&gt;JSON.parse(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static &lt;code&gt;.json&lt;/code&gt; file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;view-source:&lt;/code&gt; prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation, minification, sharing&lt;/td&gt;
&lt;td&gt;Skojio JSON Formatter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these require installing an extension. Pick the one that fits the moment.&lt;/p&gt;

</description>
      <category>json</category>
      <category>chrome</category>
      <category>developertips</category>
    </item>
    <item>
      <title>Strong passwords without a password manager — when it makes sense</title>
      <dc:creator>Skojio Community</dc:creator>
      <pubDate>Thu, 28 May 2026 15:45:43 +0000</pubDate>
      <link>https://dev.to/skojiocommunity/strong-passwords-without-a-password-manager-when-it-makes-sense-19hc</link>
      <guid>https://dev.to/skojiocommunity/strong-passwords-without-a-password-manager-when-it-makes-sense-19hc</guid>
      <description>&lt;p&gt;Password managers are the right answer for almost everything. They eliminate reuse, generate high-entropy secrets, and remove the human bottleneck. For 95% of accounts, install Bitwarden or 1Password and move on.&lt;/p&gt;

&lt;p&gt;But there is a small, specific set of accounts where a password manager is the &lt;strong&gt;wrong&lt;/strong&gt; answer. Knowing which ones — and how to handle them — is worth ten minutes of attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accounts that don't belong in a manager
&lt;/h2&gt;

&lt;p&gt;Three categories, roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The master password of the manager itself.&lt;/strong&gt; Obviously. This one lives in your head.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your primary email recovery account.&lt;/strong&gt; If you lose access to the manager and the recovery account is &lt;em&gt;also&lt;/em&gt; in the manager, you have created a circular dependency that locks you out of your own life.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk encryption keys, root passwords, and emergency-access codes for shared systems.&lt;/strong&gt; Anything you might need to type when your laptop won't boot, or read aloud to a colleague during an incident.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For these, you need passwords that are strong, memorable, and &lt;strong&gt;typeable&lt;/strong&gt;. That's a different problem from generic account passwords.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "strong" actually means
&lt;/h2&gt;

&lt;p&gt;A password is strong because of entropy — the number of equally-likely possibilities an attacker has to try. Length contributes far more entropy than character variety, and a passphrase made of random common words can hit 70+ bits of entropy while still being typeable.&lt;/p&gt;

&lt;p&gt;A 12-character random string like &lt;code&gt;K7#m$pQ2!nXz&lt;/code&gt; has roughly 79 bits of entropy. A four-word passphrase like &lt;code&gt;correct-horse-battery-staple&lt;/code&gt; has roughly 44 bits. To match the random string you need 6-7 random words.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-list rule
&lt;/h2&gt;

&lt;p&gt;For the handful of accounts that don't belong in a manager:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;List A: passphrases&lt;/strong&gt; for things you must type frequently or under stress (login, disk unlock). 6+ random words, hyphenated, lowercase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List B: high-entropy strings&lt;/strong&gt; for things stored in a sealed envelope in a safe, never typed (root password, recovery codes). 24+ random characters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For List A you can generate offline with &lt;code&gt;shuf&lt;/code&gt; or a wordlist. For List B you want a generator that runs in your browser so the secret never crosses the network.&lt;/p&gt;



&lt;p&gt;The Skojio password generator runs entirely client-side — no analytics, no server round trip — and supports both modes: configurable random strings and word-based passphrases. Use it once for each account on either list, write the result down on paper, store it appropriately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "appropriately" means
&lt;/h2&gt;

&lt;p&gt;For List A passphrases: nothing. Memorise them. If you can't, the passphrase is too long; pick a shorter one.&lt;/p&gt;

&lt;p&gt;For List B strings: a paper backup in a physically secure location. A safe deposit box, a fire-safe at home, a sealed envelope with a trusted person. Two copies in different locations is better than one.&lt;/p&gt;

&lt;p&gt;If the password ever needs to be typed by a human in an emergency, it belongs on List A. If it can be copy-pasted from paper, List B is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about MFA?
&lt;/h2&gt;

&lt;p&gt;MFA does not replace a strong password — it complements it. Every account that supports MFA should have it enabled, regardless of which list its password lives on. The categories above are about the &lt;em&gt;password&lt;/em&gt;; the second factor is a separate layer.&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;Account type&lt;/th&gt;
&lt;th&gt;Where the password lives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generic web account&lt;/td&gt;
&lt;td&gt;Password manager, randomly generated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password manager itself&lt;/td&gt;
&lt;td&gt;In your head&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email recovery account&lt;/td&gt;
&lt;td&gt;In your head + paper backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk encryption, root, emergency&lt;/td&gt;
&lt;td&gt;Paper backup, never in the manager&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The manager handles the 95% case beautifully. The 5% needs a different tool and a different storage strategy, and confusing the two is how people end up locked out of their own accounts.&lt;/p&gt;

</description>
      <category>security</category>
      <category>passwords</category>
      <category>tips</category>
    </item>
  </channel>
</rss>
