I use `JsonNode` for my dynamic type, it works great for a `kwargs` standin. I usually name it `options`, but for sake of consistency I named it `kwargs` here. I use it all over an in-production tool at work, with permission to share: import std/json import std/strformat proc get_json_value*[T: bool | float | int | string | JsonNode](node: JsonNode, keys: seq[string], default: T): T = var current_node = node for key in keys: if key in current_node: current_node = current_node[key] else: return default when T is bool: if current_node.kind == JBool: return current_node.get_bool when T is float: if current_node.kind == JFloat or current_node.kind == JInt: return current_node.get_float when T is int: if current_node.kind == JInt: return current_node.get_int when T is string: if current_node.kind == JString: return current_node.get_str when T is JsonNode: if current_node.kind in [JObject, JArray]: return current_node return default proc get_json_value*[T: bool | float | int | string | JsonNode](node: JsonNode, key: string, default: T): T = node.get_json_value(@[key], default) proc foo(a: int, b: string, kwargs: JsonNode = %*{}) = echo &"Calling foo() with {kwargs=}" let kwarg1 = kwargs.get_json_value("kwarg1", "default") echo &"{kwarg1=}" foo(1, "bar") foo(2, "baz", %*{"kwarg1": "something else"}) Run
Output: Calling foo() with kwargs={} kwarg1=default Calling foo() with kwargs={"kwarg1":"something else"} kwarg1=something else Run