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 following
Content 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 object
const 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"
user: { name: "Alice", email: "[email protected]", verified: true } // from data-attr="user"
}

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 attributes
const 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!