Is it worth defining a custom scalar even if it doesn't serialize or parse values?

I’m currently working on exposing display ready fields that include localization, eventually to split all localized fields and logic out into a dedicated subgraph.

At it’s core, the localized fields are simple strings, but I’m considering adding a LocalizedString scalar to be more explicit about what the consumer should expect. It’s not a data point, it’s a localized and display ready value. However, the scalar itself isn’t going to be doing any serializing or parsing of the value… that will be relegated to the resolvers.

Here is an example:

type Genre {
  name: String # data point or enum value like COMEDY, HORROR, etc
  l10n: LocalizedString # localized display ready field like Comedy (en-US), Comedia (es-US), etc.
}

Currency, when combining multiple data points to make a display value, may be a better example.

type AmountDue {
  value: Float
  currency: String # data point or enum value like USD, EUR,
  l10n: LocalizedString # like 'USD $10,000.50' for US USD, '10 000,50 €' for French EUR, '₹10,000.50' for Indian INR, etc.
}

These are contrived examples, but there are more relevant examples of combining data points to make a display ready value, then applying localization, in my real use cases.

But, the underlying question is, should I create a LocalizedString scalar and comment blocks as a calling card to the underlying logic or just use the default String scalar with comment blocks?

In a single subgraph, using a custom scalar makes the schema significantly clearer, even if the scalar doesn’t add any logic to the string. This approach allows you to document the returned field type separately and makes it easier to understand what the field returns just by looking at the types. I follow this practice in production today, primarily because I want the flexibility to add validation and parsing logic later without changing the type. (Note: I don’t change the output format, only the validation.)

There’s a tradeoff in a multi-subgraph federation—especially when subgraphs are owned and managed by different teams. If another subgraph later decides to implement the logic for the same scalar, you could end up with drift in how the scalar behaves across subgraphs. This issue isn’t unique to this scenario; it’s a general challenge when sharing scalars across subgraphs. A good way to mitigate this is by publishing a shared package for the scalar’s logic, even if it initially contains no additional logic. This ensures that any subgraph using the scalar gets its definition from a single source. If a subgraph later needs to introduce new logic, other subgraphs can adopt those changes simply by upgrading the package version.

3 Likes

The longer term plan is to have a dedicated Localization subgraph that will handle localization logic for any field that requires it. There may be an extended ability for domain specific subgraphs to override and own domain specific localizations, but in theory the LocalizedString scalar would mostly be applied to fields contained within the Localization subgraph.

I think my use case will sit in between your examples of a single subgraph vs multi-subgraphs.

Regardless I think you answered my question on the value of leveraging scalars to make the schema cleaner and the schema’s intent more clear.

Thanks!