json-schema-to-elm icon indicating copy to clipboard operation
json-schema-to-elm copied to clipboard

Validate pattern against regex when encoding data into JSON

Open reitzig opened this issue 8 years ago • 7 comments

Entries of type string can get a pattern, restricting the value space.

Arguably, the generated Elm code should prevent invalid JSON from being created, so it'd have to verify that the patterns match.

reitzig avatar Oct 12 '17 21:10 reitzig

By 'should prevent invalid JSON from being created' do you mean when an Elm type, Foo, is encoded back into a JSON string, encodeFoo? Note that json-schema-to-elm does not validate JSON schema files, that is the job of other tools.

dragonwasrobot avatar Apr 13 '18 12:04 dragonwasrobot

Yes, that is what I meant.

Note that json-schema-to-elm does not validate JSON schema files, that is the job of other tools.

I understand that, but not how this fact relates to my proposal? (Which is not about validating schemas or validating inputs against schemas, but generating only valid (JSON) strings out of Elm.)

reitzig avatar Apr 14 '18 20:04 reitzig

Meant no offence, just had to make sure there weren't any misunderstandings.

If possible, I'd appreciate any example JSON schemas and/or expected Elm code generated.

Depending on the use case, it might also make sense to

  1. autogenerate Elm html input forms based on the JSON schema, and
  2. let the html input make the check against the pattern instead of the encoding function.

dragonwasrobot avatar Apr 14 '18 21:04 dragonwasrobot

None taken, I was honestly confused. I guess I could have communicated that better, my bad!

Consider for instance this simple schema:

{
    "$schema": "http://json-schema.org/draft-06/schema#",
    "id": "http://some.url",
    "type": "object",
    "properties": {
      "big-numbers": {
       	"type": "string",
        "pattern": "^[0-9]+(\\.[0-9]+)?$"
      }
    }
}

The generated encoder is:

encodeNumbers : Numbers -> Value
encodeNumbers numbers =
    let
        bigNumber =
            case numbers.bigNumber of
                Just bigNumber ->
                    [ ( "bigNumber", Encode.string bigNumber ) ]

                Nothing ->
                    []
    in
        object <|
            bigNumber

We see that the pattern is not enforced; { big-numbers : Just "abc" } would just be encoded as JSON.

I would imagine something like this (please excuse my shitty Elm) could work:

bigNumbersPattern : Regex
bigNumbersPattern = Regex.regex "^[0-9]+(\\.[0-9]+)?$"

encodeNumbers : Numbers -> Value
encodeNumbers numbers =
    let
        bigNumber =
            case numbers.bigNumber of
                Just bigNumber ->
                    let 
                        bigNumberString = Encode.string bigNumber
                    in
                        case bigNumberPattern.contains bigNumberString of
                            True ->
                                [ ( "bigNumber", Encode.string bigNumber ) ]
                            False ->
                                [] -- Report error here?

                Nothing ->
                    []
    in
        object <|
            bigNumber

Side notes, just some things I noticed: When I forgot id, the error message was nice but pointed at #. When I then forgot the top-level type, the output was silently nothing. Forgetting the top-level title silently produces invalid Elm (the identifiers are empty). Having dashes in titles/names in JSON schemas (valid) silently produces invalid Elm.

reitzig avatar Apr 15 '18 09:04 reitzig

Thanks for the example code :)

Hmm, so by validating against the regex in the encoder it would necessarily end up introducing the concept of error handling to the generated Elm, i.e. so some encoders would end up returning a Maybe Value or Result String Value instead of just a Value, which again would have to be accounted for any encoder that uses that encoder.

Given that a decoder can also produce an error, this might not be so bad, but it does require a change across encoders. I'll think about it.

Regarding the bugs, I'm currently aware that there is a selections of various minor bugs that needs to be fixed and I'm planning to do a bug fixing round within the next week or two, depending on my other work loads, that should make json-schema-to-elm feel more smooth.

dragonwasrobot avatar Apr 15 '18 20:04 dragonwasrobot

Given that a decoder can also produce an error, this might not be so bad, but it does require a change across encoders. I'll think about it.

True. I don't know how much you've implemented, but JSON Schema has quite a lot of involved restrictions ("If value A is set, B should follow this schema; other wise this other one") which I don't think can be nicely handled by the type system. So if you plan to expand the coverage of JSON Schema features, error handling will probably be unavoidable, I think. If generating only conforming JSON is a requirement you want to cover, that is.

Regarding the bugs, I'm currently aware that there is a selections of various minor bugs that needs to be fixed and I'm planning to do a bug fixing round within the next week or two, depending on my other work loads, that should make json-schema-to-elm feel more smooth.

No worries. I just wanted to mention them since I know very well how you can be blind to the most obvious thing when coding your own thing. X-)

reitzig avatar Apr 15 '18 20:04 reitzig

Some quick brainstorming for doing a Result-passing version of encoders, maybe something like this (can most likely be simplified further):

-- Types

type alias User =
    { username : String
    , favoriteNumber : Maybe String
    }

type alias Error =
    String

type alias Property =
    ( String, Value )

-- Encoders

encodeObject : Result (List Error) (List Property)
encodeObject =
    Ok []

encodeUser : User -> Result (List Error) Value
encodeUser user =
    encodeObject
        |> encodeUsername user.username
        |> encodeFavoriteNumber user.favoriteNumber
        |> Result.map object -- would be nice to merge this line into the encodeObject function

encodeUsername :
    String
    -> Result (List Error) (List Property)
    -> Result (List Error) (List Property)
encodeUsername username =
    Result.map <|
        (++) [ ( "username", Encode.string username ) ]

encodeFavoriteNumber :
    Maybe String
    -> Result (List Error) (List Property)
    -> Result (List Error) (List Property)
encodeFavoriteNumber favoriteNumber =
    case favoriteNumber of
        Just number ->
            if Regex.contains (Regex.regex "^[0-9]+(\\.[0-9]+)?$") number == True then
                Result.map <|
                    (++) [ ( "bigNumber", Encode.string number ) ]
            else
                Result.mapError <|
                    (++) [ "<appropriate error message>" ]

        Nothing ->
            Result.mapError <|
                (++) [ "<appropriate error message>" ]

dragonwasrobot avatar Apr 22 '18 17:04 dragonwasrobot