# 2025-11-17
PR: https://github.com/aviatesk/JETLS.jl/pull/301
This commit completes the migration to JSON.jl v1. As explained in
aviatesk/JETLS.jl#300, JSON.jl v1 no longer supports out-of-box
deserialization for custom data types and now requires explicit
tags for each custom data type field (see
https://publish.obsidian.md/jetls/work/JETLS/Switch+JSON3.jl+to+JSON.jl+v1#2025-11-16).
This presents a significant challenge for LSP.jl, which uses the
`@interface` DSL for TypeScript-like type definitions, because
such tag generation must also be done through macro generation.
Without this approach, we would need to scatter `@tags` macros
and their associated `choosetype` definitions throughout the code
base, which should be very very tedious.
This commit further complicates the already complex `@interface`
implementation to implement such automatic tag generation.
While this macro has arguably become one of the most arcane
Julia macros in existence, it does function correctly.
Numerous tests have also been added.
Most importantly, completing the switch to JSON v1 provides the
following JSON communication performance improvements:
```julia
using LSP, JSON
function readlsp_JSON_v1(msg_str::AbstractString)
lazyjson = JSON.lazy(msg_str)
if hasproperty(lazyjson, :method)
method = lazyjson.method[]
if method isa String && haskey(LSP.method_dispatcher, method)
return JSON.parse(lazyjson, LSP.method_dispatcher[method])
end
return JSON.parse(lazyjson, Dict{Symbol,Any})
else # TODO parse to ResponseMessage?
return JSON.parse(lazyjson, Dict{Symbol,Any})
end
end
```
```julia
using JSON3, LSP
const Parsed = @NamedTuple{method::Union{Nothing,String}}
function readlsp_JSON3(msg_str::AbstractString)
parsed = JSON3.read(msg_str, Parsed)
parsed_method = parsed.method
if parsed_method !== nothing
if haskey(LSP.method_dispatcher, parsed_method)
return JSON3.read(msg_str, LSP.method_dispatcher[parsed_method])
end
return JSON3.read(msg_str, Dict{Symbol,Any})
else # TODO parse to ResponseMessage?
return JSON3.read(msg_str, Dict{Symbol,Any})
end
end
```
```julia
uri = LSP.URIs2.filepath2uri(abspath("src/JETLS.jl"))
s = """{
"jsonrpc": "2.0",
"id": 0,
"method": "textDocument/completion",
"params": {
"textDocument": {
"uri": "$uri"
},
"position": {
"line": 0,
"character": 0
},
"workDoneToken": "workDoneToken",
"partialResultToken": "partialResultToken"
}
}""";
```
```julia-repl
julia> @benchmark readlsp_JSON3(s)
BenchmarkTools.Trial: 10000 samples with 1 evaluation per sample.
Range (min … max): 363.792 μs … 1.154 ms ┊ GC (min … max): 0.00% … 0.00%
Time (median): 379.958 μs ┊ GC (median): 0.00%
Time (mean ± σ): 385.114 μs ± 16.805 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
▁▁ ▁ ▄▇█▇▆▆▅▅▅▅▄▄▄▄▃▃▃▂▂▂▂▂▁▁ ▁ ▁ ▂
██████████████████████████████████████▇▇▇▇▇▇▇▇▆▅▆▆▆▆▅▆▇▆▅▆▆▅ █
364 μs Histogram: log(frequency) by time 447 μs <
Memory estimate: 5.91 KiB, allocs estimate: 120.
julia> @benchmark readlsp_JSON_v1(s)
BenchmarkTools.Trial: 10000 samples with 1 evaluation per sample.
Range (min … max): 11.625 μs … 113.000 μs ┊ GC (min … max): 0.00% … 0.00%
Time (median): 12.375 μs ┊ GC (median): 0.00%
Time (mean ± σ): 12.658 μs ± 1.933 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
▃▄▅▅▆██▇█▅▅▄▃▂▂ ▂
▇██████████████████▇▇▆▇▆▅▇▅▅▆▆▆▇▆▆▇▇▆▇▆▆▇▇▅▇██▆▇▅▅▆▆▆▅▅▄▄▂▅▆ █
11.6 μs Histogram: log(frequency) by time 17.4 μs <
Memory estimate: 4.53 KiB, allocs estimate: 84.
```
# 2025-11-16
I talked with Jacob, the author of JSON.jl v1, and it turned out that we need to implement type dispatch using `@tags` as a result:
```julia
@tags struct Foo
x &(json=(choosetype=x->[logic to return concrete type],),)
end
```
For the `struct AB` case below, we can do something like:
```julia
@kwdef struct AB
x::Union{A, B, Nothing}
end
# Use `StructUtils.fieldtags` directly instead of `@tags` since it doesn't compose
# with `@kwdef`
StructUtils.fieldtags(::StructUtils.StructStyle, ::Type{AB}) = (; x = (json = (choosetype = function (x,)
return A
end,),))
```
# 2025-11-16
PR: https://github.com/aviatesk/JETLS.jl/pull/300
It turns out that JSON.jl has lost the functionality to parse `Union`
fields of custom data types, while JSON3.jl supports it out-of-box.
The MRE is shown below, but JETLS defines various LSP types, many of
which have such custom field types.
This was discovered when parsing `CompletionItem` in completion
resolve requests, where the `textEdit::Union{TextEdit, InsertReplaceEdit, Nothing}`
field could not be parsed (specifically, it raises `Base.convert(::Type{TextEdit, InsertReplaceEdit}, ::JSON.Object)`
method error occurred).
As a workaround, we could define `StructUtils.lower(::Type{TextEdit, InsertReplaceEdit}, ::JSON.Object)`
to manually define the conversion, but such fields likely exist many
placesin LSP.jl.
It would be hard to identify all such cases and define corresponding
`StructUtils.lower` methods, since it creates barriers for future LS
development.
For now, it'd be better to continue using JSON3.jl until this issue is
resolved on the JSON.jl side.
```julia
struct Range
start::Int
last::Int
end
struct A
a::Range
text::String
end
struct B
b::Range
text::String
end
@kwdef struct AB
x::Union{A, B, Nothing} = nothing
end
using JSON, JSON3
s = """{
"x": {
"a": {
"start": 0,
"last": 10
},
"text": "hi"
}
}""";
# works:
JSON3.read(s, AB)
# fails;
# ERROR: MethodError: Cannot `convert` an object of type
# JSON.Object{String, Any} to an object of type
# Union{A, B}
# The function `convert` exists, but no method is defined for this combination of argument types.
# Closest candidates are:
# convert(::Type{T}, ::T) where T
# @ Base Base_compiler.jl:133
# Stacktrace:
# [1] lift(::Type{Union{A, B}}, x::JSON.Object{String, Any})
# @ StructUtils ~/.julia/packages/StructUtils/BDA5D/src/StructUtils.jl:348
JSON.parse(s, AB)
```
# 2025-11-15
The migration guide of JSON.jl for JSON3.jl: [[Migration guides · JSON.jl]].
- [x] Environment setup
- [x] LSP: StructTypes.jl -> StructUtils.jl
- [x] JSONRPC: JSON3.jl -> JSON.jl v1
- [x] Manual de/serializations of LSP objects
- [x] Make `result: null` to persist in the serialized JSON
- [x] Wait for https://github.com/JuliaIO/JSON.jl/pull/408
- [x] Pass JETLS test