Back to Blog
engineering

The enum-case bug that labeled every free tool "paid"

June 17, 2026
The enum-case bug that labeled every free tool "paid"

The enum-case bug that labeled every free tool "paid"

While auditing why our FAQ snippets were converting badly in search, we found that every tool in the catalog was telling Google the same thing about its price: "paid service with various pricing tiers." Free tools, freemium tools, all of them. The data was correct in the database. The lie was three characters of casing away.

The problem

Linkgo is a curated directory of AI tools, agents, MCPs, and models — close to 800 approved entries at the time. Each tool page ships a FAQPage block of JSON-LD structured data so Google can render rich results for questions like "How much does X cost?" That answer is generated from the tool's pricingType, which we store as one of three values: free, freemium, or paid.

When we pulled up the rendered structured data during an organic-search review, the pricing answer was wrong almost everywhere. A tool that's completely free was emitting:

"Acme is a paid service with various pricing tiers."

Not a one-off. The same wrong sentence was on free tools, freemium tools, and paid tools alike. The paid tools happened to be correct — but only by accident, because "paid" was the fallback. For every free and freemium tool in the directory, we were publishing a factually wrong price to Google.

Why it happened

The pricing answer was a ternary inside the JSON-LD object:

"text": tool.pricingType === 'free'
  ? `${tool.name} is completely free to use.`
  : tool.pricingType === 'freemium'
  ? `${tool.name} offers both free and premium plans.`
  : `${tool.name} is a paid service with various pricing tiers.`

Looks fine. It isn't. tool.pricingType comes straight from a Prisma enum, and that enum is uppercase:

enum PricingType {
  FREE
  PAID
  FREEMIUM
}

So the actual value at runtime is 'FREE', never 'free'. Both equality checks — === 'free' and === 'freemium' — silently fail for every row, and the ternary slides into its final branch: "paid service with various pricing tiers." The bug wasn't in one tool's data. It was in the comparison, so it was wrong for the entire directory at once.

The part that stings: this is exactly the kind of mistake a type checker is supposed to catch. Comparing a PricingType value against the string literal 'free' should be a type error, because 'free' isn't a member of the enum. But the comparison lived inside a plain object literal being assembled for JSON-LD, where the value had already widened to a string. No type narrowing, no warning, clean tsc run, green CI. The compiler had no idea anything was off, and neither did we — until we read what Google was actually being served.

What we tried first

The instinct was to treat it as a data problem: maybe some tools had pricingType saved in the wrong case, maybe an import had lowercased things. We spot-checked rows directly in Postgres. The data was pristine — every row was a proper uppercase enum value. That ruled out a migration or a bad seed and pointed the finger back at the code reading the value, not the value itself.

The fix

Once the cause was clear, the fix was small — match the enum's real casing:

"text": tool.pricingType === 'FREE'
  ? `${tool.name} is completely free to use.`
  : tool.pricingType === 'FREEMIUM'
  ? `${tool.name} offers both free and premium plans.`
  : `${tool.name} is a paid service with various pricing tiers.`

But changing two string literals and moving on would just leave the next person free to reintroduce it. The real fix was to stop hand-comparing the enum against bare strings anywhere we render it. We pulled the enum-to-label mapping into one typed lookup:

import type { PricingType } from '@prisma/client'

const PRICING_LABEL: Record<PricingType, string> = {
  FREE: 'Free',
  FREEMIUM: 'Freemium',
  PAID: 'Paid',
}

Record<PricingType, string> is the part that earns its keep. If someone adds a fourth pricing tier to the Prisma enum later, this object stops compiling until they add the matching label. The exhaustiveness check the original ternary lacked is now enforced by the compiler, in one place, for every surface that displays pricing — the tool page, the category hubs, and the new compare pages all read from it.

Before and after

Before: every free and freemium tool in the directory — the majority of the catalog — emitted "paid service with various pricing tiers" in its FAQ structured data. After: the pricing answer matches the stored pricingType. We verified by spot-checking five known-free tools (correct "completely free" answers) and re-requesting the pages so the corrected JSON-LD would be re-crawled.

We don't have clean before/after CTR numbers to share yet — structured-data corrections take weeks to re-crawl and re-rank, and we'd rather not publish a number we made up. What we can say concretely: wrong facts in FAQ rich results are an E-E-A-T liability, and for a directory whose whole value proposition is accurate, hand-checked tool data, telling searchers that free tools cost money is about the worst small bug you can ship.

What we learned

  • Enums and string literals don't mix at the comparison site. The moment you write value === 'somestring' against a database enum, you've opted out of the one check that would catch a casing or spelling drift. Map through a typed Record<Enum, _> instead and let the compiler enforce every case.
  • A green type check is not the same as a checked type. The comparison passed tsc only because the value had widened to string inside an untyped object literal. Type safety is a property of where you write the code, not just whether the build is green.
  • Read what you actually serve. This bug was invisible in the app UI, which used a different code path, and invisible in the database. It only showed up when we looked at the raw structured data — the thing the machines downstream consume. If a surface is consumed by Google, an LLM, or another service, inspect it directly; don't assume the human-facing view tells you the truth.

What's next

The typed PRICING_LABEL closes this specific hole, but the broader lesson is that our structured data had no test coverage at all — nothing asserted that a free tool emits a free answer. That's the next thing to fix: a small snapshot test over the JSON-LD for one tool of each pricing type, so a casing regression fails CI instead of failing silently in front of a search crawler.


Try Linkgo

Linkgo is a curated directory of AI tools, agents, MCPs, and models — browse the catalog at linkgo.dev.

Website

Share

Frequently Asked Questions

What caused the pricing information to be incorrectly labeled for free and freemium tools?

The bug was due to case-sensitive string comparisons against an uppercase Prisma enum. The code compared `tool.pricingType` to lowercase strings like 'free' and 'freemium', which always failed, causing the fallback 'paid' label to be used for all tools except paid ones.

How was the enum-case bug fixed in the Linkgo directory?

The fix involved matching the enum's actual uppercase values ('FREE', 'FREEMIUM', 'PAID') in the comparison and replacing the ternary with a typed lookup object. This `Record<PricingType, string>` mapping enforces exhaustiveness and prevents casing errors in future code.

Why didn't the TypeScript compiler catch the enum-case comparison error?

Because the enum value was widened to a plain string inside an untyped object literal, the compiler lost type narrowing and did not flag the incorrect string comparisons. This shows that a green type check doesn't guarantee proper type safety in all code contexts.

What lessons were learned about maintaining accurate structured data for SEO?

Key lessons include avoiding direct string comparisons with enums, enforcing exhaustiveness with typed mappings, and always inspecting the actual structured data served to search engines rather than relying solely on UI or database correctness.

What are the next steps to prevent similar bugs in the future?

The team plans to add snapshot tests covering JSON-LD structured data for each pricing type to catch regressions in CI. This will ensure that pricing labels remain accurate and consistent in the FAQ rich results served to Google.

Continue reading

The enum-case bug that labeled every free tool "paid" | Eodin