Float field returns whole number when decimal is `.0`

Long question short, should I create a Decimal scalar? And if so is there a way to preserve it as an INT with trailing zeros instead of needing to convert it to a string?


There is a need from my consumers to always receive decimals even if the trailing values are 0.

For example, instead of receiving 10 they want to receive 10.0. This isn’t a schema-wide rule at the moment, it’s mostly specific to a handful of data-point fields in an isolated query. However, it’s not to say it won’t transcend the specific fields/query in the future. Here is additional info:

Schema:

type Query {
  getValues: Values
}

type Values {
  valueOne: Float
  valueTwo: Float
}

Query:

query GetValues {
  getValues {
    valueOne
    valueTwo
  }
}

Response:

{
  "data": {
    "getValues": {
      "valueOne": 10 // value returned from resolver is "10.0"
      "valueTwo": 10.4 // value returned from resolver is "10.4"
    }
  }
}

Resolver helper logic:

function applyValueDecimalPlaces(number: number): string {
  return number.toLocaleString('en-us', {
    maximumFractionDigits: 1,
    minimumFractionDigits: 1,
  });
}

applyValueDecimalPlaces(9.96) // returns "10.0"
applyValueDecimalPlaces(10.36) // returns "10.4"

So, the logic seems to work as expected all the way up to the resolver-schema hand off.. the resolver is providing 10.0 in this case but the response is 10. How do I ensure the consumer receives 10.0 and not 10?

do the clients need to do math on the result, or will “10.0” be display only? IOW, is this a number or a string?

1 Like

That’s a good question. We do have “l10n” fields that act as display-ready and localized strings.. those are returning “10.0 units” just fine, it’s just the number fields.

As for now, the consumers shouldn’t need to do math, the “raw data” (float) is used to populate a VisX chart library for visualizing the data.

After working with the consumers, we decided that 10 (Float) is an appropriate response for the field they will use for math / data config / etc. We have formatted fields that will be strings and return the expected display ready value of "10.0 units".

1 Like

Coming in almost 3 weeks late, and you already have an answer for your situation, but for future people who find this, I did want to share that there are reasons why this could matter, and what to do about it:

TL;DR

IMO if it matters, transport as a string and use a custom scalar (or type), but for most people it won’t ever matter.

Why it might matter (pretty much only science)

To most humans, these three numbers are the same:

  1. 10
  2. 10.0
  3. 10.00000000

Scientifically, that’s not even CLOSE to true.

How so?

Well, here are some cases where someone might use that value:

  1. “It is 10°C outside”
    • Actual value: 5 ≤ value < 15
  2. “The vaccine refrigerator is maintained at 10.0°C”
    • Actual value: 9.95 ≤ value < 10.05
  3. “We isolated 12 rubidium atoms in a magnetic-optical trap, then cooled them to precisely 10.0000000000°C. Just kidding – not even the Bose-Einstein condensate lab can measure temperature to 12 significant digits.”
    • Actual value: 9.99999999995 ≤ value < 10.00000000005

What to do

While the JSON spec only prohibits LEADING zeroes, it allows for post-decimal trailing zeroes, allowing implementations to set their own limits on range and precision. Interoperability, then, is the struggle, and I’ve found that the only way to ensure that data isn’t accidentally lost is to always use strings for transport if significant figures matter. Otherwise, someone on the other end might just JSON.parse() and then try to do something with the data, having lost everything that matters when they get back 10. Of course, they could still parseFloat and lose that data, but that’s a decision they make on an individual property basis, instead of the whole payload. In fact, even in programming languages that can parse and preserve significant figures, the input should still be provided by String.

e.g. in Java:

BigDecimal a = new BigDecimal("10.00000");
System.out.println(a.toPlainString()); // Prints 10.00000

BigDecimal b = new BigDecimal(10.00000);
System.out.println(b); // Prints 10.0

OK, so what about the GraphQL types?

Now that we’re good with transporting strings, my take is to always be as semantic as necessary (and then no further). If I had to support this use case, I would personally use a custom scalar, but I wouldn’t use Decimal, as that still doesn’t “feel like” the digits matter to the API consumer. Instead, I’d go with one of these, depending on the purpose:

  • PreciseDecimal
  • SignificantNumber

Type-based Alternative

One key alternative I thought of would be to just use a type instead of a scalar. In a setting where significant figures matter, units will probably also matter. Perhaps this:

type MeasuredValue {
  value: Float!
  significantFigures: Int! # This informs the quantity of trailing zeros
  unit: String!
}