Skip to content

Instantly share code, notes, and snippets.

@mrange
Last active February 10, 2020 21:51
Show Gist options
  • Save mrange/988ffdbeb75568aa4ed9 to your computer and use it in GitHub Desktop.
Save mrange/988ffdbeb75568aa4ed9 to your computer and use it in GitHub Desktop.
// Copyright 2015 Mårten Rånge
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
module DynamicJson =
open System
open Newtonsoft.Json.Linq
exception JsonCastException of Type
type Json =
| JsonValue of JValue
| JsonObject of JObject
| JsonArray of JArray
| JsonMissingProperty of JToken*string
| JsonIndexOutOfRange of JToken*int
| JsonUnrecognizedType of obj
static member AsJson (o : obj) =
match o with
| :? JObject as jobj-> JsonObject jobj
| :? JArray as jarr -> JsonArray jarr
| :? JValue as jval -> JsonValue jval
| _ -> JsonUnrecognizedType o
member x.Path =
match x with
| JsonValue v -> v.Path
| JsonObject o -> o.Path
| JsonArray a -> a.Path
| JsonMissingProperty (t,key) -> t.Path + "." + key
| JsonIndexOutOfRange (t,idx) -> sprintf "%s.[%d]" t.Path idx
| JsonUnrecognizedType v -> "<NULL>"
override x.ToString () =
match x with
| JsonValue v -> sprintf "Value: %s" <| v.ToString ()
| JsonObject o -> sprintf "Object: %s" <| o.ToString ()
| JsonArray a -> sprintf "Array: %s" <| a.ToString ()
| JsonMissingProperty (t,key) -> sprintf "Missing property: %s.%s" t.Path key
| JsonIndexOutOfRange (t,idx) -> sprintf "Out of range: %s.[%d]" t.Path idx
| JsonUnrecognizedType v -> sprintf "Value: %s" <| v.ToString ()
static member (?) (json : Json, key : string) : Json =
match json with
| JsonValue jval -> JsonMissingProperty (jval, key)
| JsonArray jarr -> JsonMissingProperty (jarr, key)
| JsonObject jobj ->
let p = jobj.Property key
if p <> null then
Json.AsJson p.Value
else
JsonMissingProperty (jobj, key)
| _ -> json
member x.Item (idx : int) : Json =
match x with
| JsonValue jval -> JsonIndexOutOfRange (jval, idx)
| JsonObject jobj -> JsonIndexOutOfRange (jobj, idx)
| JsonArray jarr ->
if idx < 0 then
JsonIndexOutOfRange (jarr, idx)
elif idx >= jarr.Count then
JsonIndexOutOfRange (jarr, idx)
else
Json.AsJson jarr.[idx]
| _ -> x
member x.HasValue =
match x with
| JsonValue jval -> true
| _ -> false
member x.CastTo<'T> () =
match x with
| JsonValue jval -> jval.ToObject<'T>()
| JsonIndexOutOfRange (t, idx) ->
raise (IndexOutOfRangeException (x.ToString ()))
| _ ->
raise (InvalidCastException ())
let asJson (o : obj) = Json.AsJson o
let hasValue (json : Json) = json.HasValue
let castTo<'T> (json : Json) = json.CastTo<'T> ()
let describe (json : Json) = json.ToString ()
module Test =
let json = """
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"id": 24,
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
"""
open DynamicJson
open Newtonsoft.Json
open Test
[<EntryPoint>]
let main argv =
let json = asJson <| JsonConvert.DeserializeObject Test.json
let missing = describe json?glossary?missingProp?nextProp.[1]
let seeAlso = castTo<string> json?glossary?GlossDiv?GlossList?GlossEntry?GlossDef?GlossSeeAlso.[1]
printfn "%A" missing
printfn "%A" seeAlso
0
@mrange
Copy link
Author

mrange commented Feb 8, 2020

I have been tinkering a bit with a newer version to support dynamic access to json array and objects. If you are interested you can take a look, it's quite rough at the moment but perhaps you see where I am going from the examples: https://gist.github.com/mrange/2f26f0e37d92ac616c88d4665742d88c

I realized I probably should rethink what a query is and by doing so I think I can increase the compasability of the queries over arrays

@NickDarvey
Copy link

I actually ended up with something like your first example, json.glossaries.ToArray |> Array.map (fun j -> j?title), on Friday.

module DynamicJson =
  let (|?|) (json : Json) (f : Json -> Json) : Json seq =
    match json with
    | JsonValue  jval -> Seq.singleton <| JsonNotAnArray jval
    | JsonObject jobj -> Seq.singleton <| JsonNotAnArray jobj
    | JsonArray  jarr -> Seq.map (Json.ToJson >> f) <| jarr
    | _               -> Seq.singleton json

let ids = result?messages|?|(fun x -> x?message_id)  |> Seq.map castTo<string>

but where you're going with F# Dynamic JSON #2 is looking so good. Thank you for this!

@mrange
Copy link
Author

mrange commented Feb 10, 2020

Perhaps it would be better to base this around the JSON API in dotnet core rather than NewtonSoft JSON.

@mrange
Copy link
Author

mrange commented Feb 10, 2020

Well that didn't work out as System.Text.Json doesn't have a writable DOM

@mrange
Copy link
Author

mrange commented Feb 10, 2020

Well, I thought I create repo around the F# navigate ideas. Feel free to have a look at the example programs and jot down some ideas if you have them

https://github.com/mrange/FsJsonNavigation/blob/master/src/FsJsonNavigation/FsNavigation.Example.DotNetCore/Program.fs#L68

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment