Repository: godotengine/godot (language: cpp)

Issue:
Regression: LSP sends unparsable message, breaking text editor lsp integration.
### Tested versions

- Not Reproducible in 4.3
- Reproducible in 4.4 dev

### System information

Godot v4.4.dev (efa144396) - Fedora Linux 41 (Workstation Edition) on Wayland - X11 display driver, Multi-window, 2 monitors - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 3090 (nvidia; 565.77) - AMD Ryzen 9 5950X 16-Core Processor (32 threads)

### Issue description

My lsp (helix-lsp) leaves the following error log when trying to parse a message from Godot 4.4 LSP

helix_lsp::transport [ERROR] Exiting gdscript after unexpected error: Parse(Error("data did not match any variant of untagged enum ServerMessage", line: 0, column: 0))


### Steps to reproduce

1. Open Godot
2. Open gdscript file in project in helix using netcat to connect to lsp
3. Language server immediately exits

Issue does not exist in 4.3 stable


### Minimal reproduction project (MRP)

Any gdscript will crash it.


Discussion / hints:
This seems to be caused by the change to float serialization in #47502. Helix sends `"id": 0` which Godot will read as float, thus Helix logs `"id": 0.0` for the response, when adding an int conversion for id, helix connects fine. 

However solving this correctly is actually a bit though, since technically we are spec compliant and helix isn't (from my understanding of the spec). JSONRPC is json based so on protocol level there is only a number type. So whether we send `"id": 0` `"id": 0.0` _should_ not make a difference to the client.

Easiest fix would probably be using the old serialization for JSONRPC, but I don't know if there is a way to do that (@KoBeWi is there?).
Good ole JSON
Wait why would an id of anything be a float?
To my knowledge it would make sense to have any sort of ID be a string in JSON since you don't want float or int confusion, as well as the fact that id's aren't to be used for math or anything so having it as a string is always the safest.

Is this an issue with the JSONRPC, LSP spec, and not really helix or godot issue?

Im supposing that prior to the PR, Godot would treat id as an int, and thus work?
JSONRPC allows the client to pick the ids. Both numbers (and thus floats since json doesn't differentiate) and strings are allowed [as per the protocol](https://www.jsonrpc.org/specification#request_object), so we need to support both. Godot doesn't treat it as int, it never interacts with it and just safes the `Variant` from the request and uses it for the response. Since all json numbers are loaded as floats the variant contains a float in this case. It worked before since Godot would serialize floats of the form `x.0` as `"x"`, this was changed to differentiate types in the Godot console, but it seems to have some unintended side effects.

But again on protocol level there are only numbers, so it kinda isn't our fault technically. On the other hand I can totally see why no one would account for floating points when writing an LSP Client (especially when not writing in js this could be quite complicated). So fixing this on our side would make sense to me.

Edit: As pointed out on the Helix side: the LSP spec actually limits id's to integers https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage. This doesn't effect the issue in my opinion though, since this integer limitation is value based and can't be represented by json type wise. LSP integer is just a typedef for `number` with the restriction that it must contain integers. There are no additional requirements regarding json serialization (or I didn't find them) so serializing an LSP integer as `0.0` in json should by fine from my understanding.
Thank you for looking into this.
To my understanding, you are saying that helix just expects Godot to echo back the same id, and since Godot is not, due to the adding of .0  helix parsing fails because it probably is using serde or something and expecting an int, since it sent an int.

If the internal change was really just intended to affect console output. Maybe it is best to revert the change, and instead just change the Debug trait (or Godot cpp equivalent) for JSON "numbers" instead of printing it, rather then changing the source data of all JSON parsing...

JSON having a "Number" type causes issues when JSON parsers have differing levels of strictness in parsing 1.0 vs 1
which I would suppose matters in the case of, likely serde on helix's end.
> Easiest fix would probably be using the old serialization for JSONRPC, but I don't know if there is a way to do that

There is a workaround:

https://github.com/godotengine/godot/blob/efa144396d8003c4b3021bca8242ce5cda4d131f/core/extension/extension_api_dump.cpp#L1374-L1376

Similar thing can be likely applied in the JSONRPC.
This might be possible when serializing the values on their own, but I don't really want to doctor around in the string resulting from serializing a dictionary.

This could probably solved for now by doing an integer conversion of the id value received from the client if it is a number and has a .0 fraction. This wouldn't really solve the serialization issue, but given that we use the correct number types for most other values, `id` is probably the only effected attribute in practice.
@HolonProduction The change I made works for me. 
I haven't tested in other envs/editors but this just preserves the same behavior before the regression.
I would assume that this would not affect other editors that worked before #47502

Fix patch:
diff --git a/modules/jsonrpc/jsonrpc.cpp b/modules/jsonrpc/jsonrpc.cpp
index a3c0ce88bf5d..0f03e49fc8b7 100644
--- a/modules/jsonrpc/jsonrpc.cpp
+++ b/modules/jsonrpc/jsonrpc.cpp
@@ -122,6 +122,11 @@ Variant JSONRPC::process_action(const Variant &p_action, bool p_process_arr_elem
 		Variant id;
 		if (dict.has("id")) {
 			id = dict["id"];
+
+			// Account for implementations that discern between int and float on the json serialization level, by using an int if there is a .0 fraction. See #100914
+			if (id.get_type() == Variant::FLOAT && id.operator float() == (float)(id.operator int())) {
+				id = id.operator int();
+			}
 		}
 
 		if (object == nullptr || !object->has_method(method)) {


Test patch: