Number vs Float discrepancy between simulator and device

I’ve run into a weird issue and would like your opinions.

I have code that performs some calculations that should produce a Number, and then instantiates an object whose constructor expects a Number.

The compiler seems happy that these calculations result in a Number and allows me to pass the result into the constructor. Later, I store that value in a member typed as Object, and later convert it back to a Number again. At that point I had a safety instanceof check to confirm the Object really is a Number.

In the simulator everything works fine, but on some real devices (under some conditions) it looks like the calculation results in a Float. Monkey C “duck typing” then lets the Float propagate without complaint until my explicit check, which is how I noticed.

To me the core issue is that the compiler believes the code results in a Number, the simulator behaves accordingly, but real devices sometimes behave differently.

Here is the relevant code (with comments):

var minValue = sitemapNumeric.getMinValue(); // returns a Number
var maxValue = sitemapNumeric.getMaxValue(); // returns a Number
var step     = sitemapNumeric.getStep();     // returns a Number

var currentValue =
    numericItem.hasState()
    ? numericItem.getNumericState() // returns a Number
    : (minValue + Math.round((maxValue - minValue) / (2 * step)) * step).toNumber();

for (var i = minValue; i <= maxValue; i += step) {
    if (_currentIndex == -1 && currentValue <= i) {
        _currentIndex = _pickables.size();
        if (i != currentValue) {
            _nonConforming = new NumericPickable(currentValue, unit); // expects Number
            _pickables.add(_nonConforming);
        }
    }
    _pickables.add(new NumericPickable(i, unit)); // expects Number
}

And here is NumericPickable:

class NumericPickable extends CustomPickable {
    public function initialize(value as Number, unit as String) {

        // Workaround: force it to be a Number, so later reads are consistent
        value = value.toNumber();

        // CustomPickable stores the first parameter as Object
        CustomPickable.initialize(value, value.toString() + unit);
    }
}

Has anyone seen something like this before?

Once I understand what’s going on, the workaround is easy (the extra toNumber() in NumericPickable). But I’d really like to understand why the compiler and simulator treat this as a Number, while some real devices end up producing a Float.

If anyone is interested, here are the full source files. The first file contains the calculation discussed above, starting at line 39:

https://github.com/openhab/openhab-garmin/blob/main/source/user-interface/control-views/numeric/NumbericPickerFactory.mc

https://github.com/openhab/openhab-garmin/blob/main/source/user-interface/control-views/numeric/NumericPickable.mc

  • 1. can you share the mock-up so we understand why it doesn't reproduce it, and maybe fix it? (though I doubt we'll be able)

    2. can you open a bug report? If I understand correctly then at least some physical devices parse 5 (I mean: {"foo":5} ) which is a bug even if I make sure that the api always sends 5 and not 5.0

  • 1. can you share the mock-up so we understand why it doesn't reproduce it, and maybe fix it? (though I doubt we'll be able)

    Yes, I’m using Postman. Here is a public URL to the mock-up.

    https://60a076bf-5bc5-4098-8d14-d1cbebd92b01.mock.pstmn.io:443/rest/sitemaps/uicomponents_sitemap_test

    There is only a single step field, so it’s easy to pinpoint where the issue occurs.

    2. can you open a bug report? If I understand correctly then at least some physical devices parse 5 (I mean: {"foo":5} ) which is a bug even if I make sure that the api always sends 5 and not 5.0

    At the moment, I’m not entirely sure what to report. My initial assumption that there was a difference between the simulator and the real device has turned out to be incorrect. The difference appears to lie in how the server delivers the JSON, and I think I first need to determine exactly what that difference is before filing a bug report.

  • If I understand correctly then at least some physical devices parse 5 (I mean: {"foo":5} ) which is a bug even if I make sure that the api always sends 5 and not 5.0

    I think you're missing some words here. I assume you mean "some physical devices parse 5 as a Float". 

    It's still not clear to me what the behaviour is *supposed* to be. Too bad none of this stuff is documented - if there's a spec, we don't get to see it. If it was documented, then we could at least distinguish between a bug and "design we don't like". If the expected behaviour was documented, it would also seemingly be much less likely for the behaviour to be different over time, across devices, or between a real device and a simulated device.

    Some interesting context on this from a 2022 bug report:

     Media.SyncDelegate JSON parsing bug in CIQ 4.1.5 

    Given the following JSON payload: {"foo": 42}

    If makeWebRequest fetches that payload from outside a Media.SyncDelegate context, 42 is parsed as Lang.Number. However, if the makeWebRequest fetch is made within a Media.SyncDelegate context, it will be parsed as a Lang.Float.

    This only occurs in CIQ 4.1.5. Previous versions don't exhibit this behavior.

    --

    This appears to be a simulator bug; when I try it on an actual Fenix 6, the 42 is parsed a Lang.Number even when called within a Media.SyncDelegate context.

    ---

    I think it's worth posting a bug report to try to determine what the "correct" behaviour should be.

    But if it's true that some real devices will always parse a JSON number as a Float (even if it lacks a decimal point), and other devices will selectively parse as Float or Number, then for practical purposes, apps can't make any assumption about which behaviour they'll see.

    Irl we want our apps to work, even if it means grudgingly working around bugs or design issues.

  • If you have access to a server, then you could try the following: put a json file there, and set it up somehow so that you'll be able to request different urls, which all return the json. but with different content-type. Then look at each url with Monkey C and see if you find which content-type behaves how. 

    If that doesn't help, then play with transfer-encoding (might be a bit more tricky)

    Ah, you know what, maybe before all this, can you post all headers of the real server's response and of the mock, so we can look at it and maybe we'll spot something?

  • I think it's safe to say that if you would ask 100 Monkey C developers, they would expect {"number":5, "float":5.0}. I haven't met yet any framework or language where that is not the case.

  • So, here are the response headers from the affected user. In his setup, nginx sits between the app and the openHAB server:

    content-encoding: gzip 
    content-type: application/json 
    date: Thu,22 Jan 2026 11:13:31 GMT 
    server: nginx 
    vary: Accept-Encoding,User-Agent 
    x-robots-tag: noindex,nofollow,nosnippet,noarchive

    For comparison, these are the headers I receive directly from openHAB. In my environment, the issue is not reproducible.

    Content-Encoding: gzip
    Content-Type: application/json
    Date: Thu, 22 Jan 2026 07:52:19 GMT
    Server: Jetty(9.4.57.v20241219)
    Transfer-Encoding: chunked
    Vary: Accept-Encoding, User-Agent

    And, possibly relevant as well, here is what the Postman mock server returns. With the Postman mock server the issue did not occur on the user's watch.

    X-Firefox-Spdy: h2
    access-control-allow-origin: *
    cf-cache-status: DYNAMIC
    cf-ray: 9c1d7a44ac785bbe-VIE
    content-encoding: gzip
    content-type: application/json; charset=utf-8
    date: Thu, 22 Jan 2026 07:53:26 GMT
    server: cloudflare
    vary: Accept-Encoding, User-Agent
    x-envoy-upstream-service-time: 165
    x-ratelimit-limit: 120
    x-ratelimit-remaining: 119
    x-ratelimit-reset: 1769068466
    x-srv-span: v=1;s=b0f149a962dee3eb
    x-srv-trace: v=1;t=0002518baa6321b1

    I’m now in contact with a second user who has experienced the same issue, so I’ll try to determine whether there’s a common denominator. It’s also possible that the phone’s operating system plays a role, although I’m not sure how or to what extent the phone processes or forwards the JSON to the watch.

  • Can the postman do:

    change the content-type to be without charset?

    make the transfer-encoding: chunked (this obviously means not only to change the header, but to also act accordingly)?

    Regarding the phone: I think this all goes through the Garmin Connect app, so not only the OS, but even the GC version can mess it up. Also GC might download all the chunks and send the whole in one big push to the Garmin device. We don't know, and probably we'll never know

  • I doubt this is going to change anything.

    The chars used to express numbers are the same.