The Tiny JSON Parser That Fixes Your CSP Headaches
- Published
You're building a React app with server-side rendering. Everything's going smoothly until you need to pass some initial data from your server to kick-start the client. Maybe it's user preferences, API endpoints, or feature flags. What's the first thing that comes to mind?
If you're like most developers, you probably reach for the inline script approach:
html
<script>window.APP_CONFIG = {userId: 12345,theme: "dark",features: ["new-dashboard", "beta-search"]};</script>
It works perfectly... until it doesn't.
The CSP problem nobody talks about
Here's what happens when you try to ship that code to production with a proper Content Security Policy (which you should absolutely have):
Refused to execute inline script because it violates the followingContent Security Policy directive: "script-src 'self'"
Ouch. Your app breaks, your data doesn't load, and you're left scrambling for a solution.
Sure, you could add 'unsafe-inline'
to your CSP, but that's like leaving your front door wide open because you lost your keys. You could generate nonces for every request, but now you've added complexity and made caching a nightmare.
There had to be a better way.
Meet json-from-script
What if I told you there's a tiny library that solves this problem completely? No CSP violations, no caching issues, no complex setup. Just clean, declarative data transport that works everywhere.
json-from-script does exactly that.
Let's see it in action.
How to use it with plain HTML
Instead of executable JavaScript that triggers CSP violations, we use <script type="application/json">
tags. These don't execute code, so they're perfectly safe:
html
<!DOCTYPE html><html><head><title>My App</title></head><body><div id="app"></div><!-- This is CSP-friendly because it doesn't execute --><script type="application/json" class="data" data-attr="config">{"userId":12345,"theme":"dark","features":["new-dashboard","beta-search"]}</script><script type="application/json" class="data" data-attr="user">{"name":"Alice","email":"[email protected]","verified":true}</script><script src="/bundle.js"></script></body></html>
Notice how we're using data-attr
to give each JSON block a name. The library will use these attribute values as the keys in the final object - so data-attr="config"
becomes the config
property.
Reading the data in React
Now here's where the magic happens. In your React component:
jsx
import React, { useState, useEffect } from 'react';import jsonFromScript from 'json-from-script';const App = () => {const [appData, setAppData] = useState(null);useEffect(() => {// This parses all JSON script tags and returns a single objectconst data = jsonFromScript();setAppData(data);}, []);if (!appData) {return <div>Loading...</div>;}return (<div className={`app theme-${appData.config.theme}`}><header><h1>Welcome, {appData.user.name}!</h1>{appData.user.verified && <span className="verified">✓ Verified</span>}</header><main>{appData.config.features.includes('new-dashboard') ? (<NewDashboard userId={appData.config.userId} />) : (<LegacyDashboard userId={appData.config.userId} />)}</main></div>);};
The jsonFromScript()
call scans the DOM for all <script class="data">
tags, reads their JSON content, and creates a single object where each property name comes from the data-attr
value:
js
{config: { userId: 12345, theme: "dark", features: [...] }, // from data-attr="config"}
Why this approach actually works
Let's talk about why this is better than the alternatives:
CSP compliance without compromise. Those <script type="application/json">
tags don't execute, so strict CSP policies love them. No nonces, no 'unsafe-inline'
, no security trade-offs.
Independent caching. Since these aren't inline scripts, your CDN can cache them separately. Update your app logic? The data stays cached. Change your configuration? Your app bundle stays cached.
Error handling that makes sense. Invalid JSON throws the same error you'd get from JSON.parse()
, making it easy to catch configuration problems during development:
jsx
useEffect(() => {try {const data = jsonFromScript();setAppData(data);} catch (error) {console.error('Invalid JSON in script tag:', error);// Handle the error appropriately}}, []);
Customizing the behavior
The default behavior looks for script.data
elements and uses data-attr
for the property names. But you can customize both the CSS selector and the attribute name:
jsx
// Look for different script tags with different attributesconst data = jsonFromScript('script.app-config', 'data-key');
This would parse HTML like:
html
<script type="application/json" class="app-config" data-key="settings">{"darkMode": true, "language": "en"}</script>
And return:
js
{settings: { darkMode: true, language: "en" } // "settings" comes from data-key="settings"}
Real-world example: Next.js-style data loading
Here's how you might use this pattern in a blog application. First, your server renders the data as JSON script tags:
html
<!-- Server renders this --><script type="application/json" class="data" data-attr="pageProps">{"posts":[{"id":1,"title":"Hello World","excerpt":"My first post"}],"currentUser":{"id":42,"name":"Alice"}}</script><script type="application/json" class="data" data-attr="appConfig">{"apiUrl":"https://api.example.com","version":"2.1.0","features":["comments","sharing"]}</script>
Then your React component parses all the data at once:
jsx
import React, { useMemo } from 'react';import jsonFromScript from 'json-from-script';const BlogPage = () => {const { pageProps, appConfig } = useMemo(() => {return jsonFromScript();}, []);return (<div><header><h1>My Blog</h1><p>v{appConfig.version} | Welcome, {pageProps.currentUser.name}!</p></header><main>{pageProps.posts.map(post => (<article key={post.id}><h2>{post.title}</h2><p>{post.excerpt}</p>{appConfig.features.includes('sharing') && (<button>Share this post</button>)}</article>))}</main></div>);};
When this approach shines
This pattern is particularly useful for:
Widget embeds where you need configuration from the host page:
html
<div id="my-widget"></div><script type="application/json" class="data" data-attr="widget">{"color":"blue","size":"large","showBorder":true}</script>
Progressive enhancement when adding React to existing server-rendered pages:
html
<!-- Existing server-rendered content --><div class="legacy-content">...</div><!-- New React component with data --><div id="react-component"></div><script type="application/json" class="data" data-attr="componentData">{"items":[...],"settings":{...}}</script>
Micro-frontends sharing configuration between different applications on the same page.
Getting started
Ready to solve your CSP data bootstrapping problems? Check out json-from-script on GitHub for installation instructions and complete documentation.
Then replace your inline scripts with JSON script tags, and you're done. No more CSP violations, no more caching headaches, no more reinventing the wheel for every project.
At just a tiny size, it's the kind of focused tool that solves one problem really well. Sometimes that's exactly what you need.
Closing thoughts
The json-from-script
approach might seem simple, but that's its strength. It leverages existing web standards (<script type="application/json">
has been around for years), works with all CSP policies, and requires minimal setup.
Next time you're wrestling with inline scripts and CSP violations, remember there's a cleaner way. Your security team will thank you, your CDN will thank you, and your future self will thank you.
Have you run into similar CSP challenges in your projects? I'd love to hear about your solutions!