Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern matching on provided types

Firstly, obtain a schema and parse:

type desc = JsonProvider< """[{"name": "", "age": 1}]""", InferTypesFromValues=true >
let json = """[{"name": "Kitten", "age": 322}]"""
let typedJson = desc.Parse(json)

Now we can access typedJson.[0] .Age and .Name properties, however, I'd like to pattern match on them at compile-time to get an error if the schema is changed.

Since those properties are erased and we cannot obtain them at run-time:

let ``returns false``() = 
  typedJson.[0].GetType()
    .FindMembers(MemberTypes.All, BindingFlags.Public ||| BindingFlags.Instance, 
                 MemberFilter(fun _ _ -> true), null) 
  |> Array.exists (fun m -> m.ToString().Contains("Age"))

...I've made a runtime-check version using active patterns:

let (|Name|Age|) k = 
  let toID = NameUtils.uniqueGenerator NameUtils.nicePascalName
  let idk = toID k
  match idk with
  | _ when idk.Equals("Age") -> Age
  | _ when idk.Equals("Name") -> Name
  | ex_val -> failwith (sprintf "\"%s\" shouldn't even compile!" ex_val)

typedJson.[0].JsonValue.Properties()
|> Array.map (fun (k, v) -> 
     match k with
     | Age -> v.AsInteger().ToString() // ...
     | Name -> v.AsString()) // ... 
|> Array.iter (printfn "%A")

In theory, if FSharp.Data wasn't OS I wouldn't be able to implement toID. Generally, the whole approach seems wrong and redoing the work.

I know that discriminated unions can't be generated using type providers, but maybe there's a better way to do all this checking at compile-time?

like image 927
yuyoyuppe Avatar asked Jul 16 '26 11:07

yuyoyuppe


2 Answers

If you use InferTypesFromValues=false, you get a strong type back:

type desc = JsonProvider< """[{"name": "", "age": 1}]""", InferTypesFromValues=false >

You can use the desc type to define active patterns over the properties you care about:

let (|Name|_|) target (candidate : desc.Root) =
    if candidate.Name = target then Some target else None

let (|Age|_|) target (candidate : desc.Root) =
    if candidate.Age = target then Some target else None

These active patterns can be used like this:

let json = """[{"name": "Kitten", "age": 322}]"""
let typedJson = desc.Parse(json)

match typedJson.[0] with
| Name "Kitten" n -> printfn "Name is %s" n
| Age 322m a -> printfn "Age is %M" a
| _ -> printfn "Nothing matched"

Given the typedJson value here, that match is going to print out "Name is Kitten".

like image 69
Mark Seemann Avatar answered Jul 22 '26 11:07

Mark Seemann


As far as I know it cannot be possible to find out if "Json schema has changed" at compile-time using the given TP.

That's why:

  • JsonProvider<sample> is exactly what kicks in at compile-time providing a type for manipulating Json contents at run-time. This provided erased type has couple of run-time static methods common for any sample and type Root extending IJsonDocument with few instance properties including ones based on compile-time provided sample (in your case - properties Name and Age).There is exactly one very relaxed implicit Json "schema" behind JsonProvider-provided type, no another such entity to compare with for change at compile-time;
  • at run-time only provided type desc with its static methods and its Root type with correspondent instance methods are at your service for manipulating arbitrary Json contents. All this jazz is pretty much agnostic with regard to "Json schema", in your given case as long as run-time Json contents represent an array its elements may be pretty much any. For example,
  • type desc = JsonProvider<"""[{"name": "", "age": 1}]"""> // @ compile-time
    
    let ``kinda "typed" json`` = desc.Parse("""[]""") // @ run-time
    let ``another kinda "typed" json`` =
        desc.Parse("""[{"contents":"whatever", "text":"blah-blah-blah"},[{"extra":42}]]""")
    

    both will be happily parsed at run-time as "typed Json" conforming to "schema" derived by TP from the given sample, although apparently Name and Age are missing and exceptions will be raised if accessed. It is doable building another Json TP that relies upon a formal Json schema. It may consume reference to the given schema from a schema repository upon type creation and allow manipulating elements of the parsed Json payload only via provided accessors derived from the schema at compile-time.

    In this case change of the referred schema may break compilation if provided accessors being used in the code are incompatible with the change. Such arrangement accompanied by run-time Json payload validator or validating parser may provide reliable enterprise-quality Json schema change management.

    JsonProvider TP from Fsharp.Data lacks such Json schema handling abilities, so payload validations are to be done in run-time only.

    like image 45
    Gene Belitski Avatar answered Jul 22 '26 10:07

    Gene Belitski