# 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