Elm. Comfortable and awkward. Json.Encoder and Json.Decoder

    We continue to talk about Elm 0.18 .


    Elm. Comfortable and awkward
    Elm. Comfortable and awkward. Composition
    Elm. Comfortable and awkward. Http, Task


    In this article we will consider the issues of encoders / decoders.


    Decoders / Encoders are used for:


    1. conversion of responses from third-party resources (Http, WebSocket, etc.);
    2. port interactions. In more detail about ports and the native code I will tell in the following articles.

    As described earlier, Elm requires us to convert external data into internal application types. The Json.Decode module is responsible for this process . Reverse process - Json.Encode .


    The type that defines decoding rules is Json.Decode.Decoder a . This type is parameterized by the user type and determines how to get the user type a from the JSON object.


    For the encoder, only the result type is defined - Json.Encode.Value .


    Consider examples for the type UserData.


    typealiasUser =
      { id: Int
      , name: String
      , email: String
      }

    Decoder for receiving user data:


    decodeUserData : Json.Decode.Decoder UserData
    decodeUserData =
      Json.Decode.map3 UserData
        (Json.Decode.field “id” Json.Decode.int)
        (Json.Decode.field “nameJson.Decode.string)
        (Json.Decode.field “email” Json.Decode.string)
    encodeUserData : UserData -> Json.Encode.Value
    encodeUserData userData =
      Json.Encode.object
        [ ( “id”, Json.Encode.int userData.id)
        , ( “name”, Json.Encode.string userData.name)
        , ( “email”, Json.Encode.string userData.email)
        ]

    The Json.Decode.map3 function accepts a UserData type constructor. Next, three type decoders are transmitted according to the order in which they are declared in the UserData user type.


    The decodeUserData function can be used in conjunction with the Json.Decode.decodeString or Json.Decode.decodeValue functions. Example of use from previous articles.


    The encodeUserData function encodes a custom type into a Json.Encode.Value type, which can be sent out. By simple, Json.Encode.Value corresponds to a JSON object.


    Simple options are described in the documentation, they can be studied without much difficulty. Let's look at life situations that require some finger dexterity.


    Union type decoders or type discriminators


    Suppose we have a catalog of goods. And each product can have an arbitrary number of attributes, each of which is of the type one of the set:


    1. integer;
    2. line;
    3. enumerated. Assumes a choice of one of the valid values.

    JSON object is of the following form:


    {
      “id”: 1,
      “name”: “Product name”,
      “price”: 1000,
      “attributes”: [
        {
          “id”: 1,
          “name”: “Length”,
          “unit”: “meters”,
          “value”: 100
        }, 
        {
          “id”: 1,
          “name”: “Color”,
          “unit”: “”,
          “value”: {
            “id”: 1,
            “label”: “red”
          }
        },...
      ]
    }

    The remaining possible types will not be considered, working with them is similar. Then a custom item type would have the following description:


    typealias Product = 
      { id: Int
      , name: String
      , price: Int
      , attributes: Attributes
      }
    typealias Attributes = List AttributetypealiasAttribute = 
      { id: Int
      , name: String
      , unit: String
      , value: AttributeValue
      }
    type AttributeValue
      = IntValue Int
      | StringValue String
      | EnumValue Enum
    typealias Enum = 
      { id: Int
      , label: String
      }

    Lightly discuss the types described. There is a product (Product) that contains a list of attributes / characteristics (Attributes). Each attribute (Attribute) contains an identifier, a name, a dimension, and a value. An attribute value is described as a union type, one for each type of characteristic value. The Enum type describes one value from the allowed set and contains: an identifier and a human readable value.


    Description of the decoder, the prefix Json.Decode omitted for brevity:


    decodeProduct : Decoder Product
    decodeProduct =
      map4 Product
        (field “id” int)
        (field “name” string)
        (field “price” int)
        (field “attributes” decodeAttributes)
    decodeAttributes : Decoder Attributes
    decodeAttributes =
      list decodeAttribute
    decodeAttribute : Decoder Attribute
    decodeAttribute = 
      map4 Attribute
       (field “id” int)
       (field “name” string)
       (field “unit” string)
       (field “value” decodeAttributeValue)
    decodeAttributeValue : Decoder AttributeValue
    decodeAttributeValue =
      oneOf 
        [ map IntValue int
        , map StringValue string
        , map EnumValue decodeEnumValue
        ]
    decodeEnumValue : Decoder Enum
    decodeEnumValue = 
      map2 Enum
        (field “id” int)
        (field “label” string)

    The whole trick is contained in the decodeAttributeValue function. Using the Json.Decode.oneOf function, all valid decoders for an attribute value are searched. In case of successful decompression by one of the decoders, the value is tagged with the corresponding tag from the AttributeValue type.


    The encoding of the Product type can be performed using the Json.Encode.object function, to which the encoded type attributes will be passed. It is worth paying attention to the coding of the AttributeValue type. In accordance with the previously described JSON object, the encoder can be described as, the prefix Json.Encode is omitted for brevity:


    encodeAttributeValue : AttributeValue -> Value
    encodeAttributeValue attributeValue = 
      case attributeValue of
        IntValue value -> 
          intvalue
        StringValue value -> 
          string value
        EnumValue value ->
          object
            [ (“id”, intvalue.id)
            , (“id”, string value.label)
            ]

    As you can see, we compare the type options and use the appropriate encoders.


    Let's change the description of attributes and define them using a type discriminator. The attribute JSON object, in this case, would look like this:


    {
       “id”: 1,
       “name”: “Attributename”,
       “type”: “int”,
       “value_int”: 1,
       “value_string”: null,
       “value_enum_id”: null,
       “value_enum_label”: null
    }

    In this case, the type discriminator is stored in the type field and determines in which field the value is stored. Such a description structure is probably not the most convenient, but often encountered. Whether it is worth changing the type description for this JSON object is probably not worth it; it is better to keep the types in a convenient form for internal use. In this case, the description of the decoder may be as follows:


    decodeAttribute2 : Decoder Attribute
    decodeAttribute2 =
     field "type"string
      |> andThen decodeAttributeValueType
      |> andThen (\attributeValue ->
         map4 Attribute
            (field "id" int)
            (field "name"string)
            (field "unit"string)
            (succeed attributeValue)
      )
    decodeAttributeValueType : String -> Decoder AttributeValue
    decodeAttributeValueType valueType =
     case valueType of"int" ->
         field "value_int" int
           |> Json.Decode.map IntValue
       "string" ->
         field "value_string"string
           |> Json.Decode.map StringValue
       "enum" ->
         map2 Enum
           (field "value_enum_id" int)
           (field "value_enum_label"string)
           |> Json.Decode.map EnumValue
       _ ->
         Json.Decode.fail "Unknown attribute type"

    In the decodeAttribute2 function, we first decode the discriminator; in case of success, we decode the attribute value. Next, we decode the remaining fields of the Attribute type, and specify the previously obtained value as the value of the value field.


    Source code decoder .


    Partial type update


    There are cases when the API does not return the entire object, but only a part of it. For example, when registering to view or change the status of the object. In this case, in the message it is more convenient to immediately receive the updated object, and to hide all manipulations behind the decoder.


    For example, take the same product, but add a status field to it and process the request to close the product.


    typealias Product = 
      { id: Int
      , name: String
      , price: Int
      , attributes: Attributes
      , status: Int
      }
    decodeUpdateStatus : Product -> Decoder Product
    decodeUpdateStatus product = 
      field “status” int
        |> andThen (\newStatus ->
          succeed { product | status = newStatus}
        )

    Or you can use the Json.Decode.map function.


    decodeUpdateStatus : Product -> Decoder Product
    decodeUpdateStatus product = 
      field “status” int
        |> map (\newStatus ->
          { product | status = newStatus}
        )

    date and time


    We will use the Date.fromString function, which is implemented using a Date type constructor.


    decodeDateFromString : Decoder Date.Date
    decodeDateFromString = 
      string
        |> andThen (\stringDate ->
          caseDate.fromString stringDate of
            Ok date -> Json.Decode.succeed date
            Err reason -> Json.Decode.fail reason
        )

    If the Timestamp is used as the date / time representation, then the decoder in general can be described as:


    decodeDateFromTimestamp : Decoder Date.Date
    decodeDateFromTimestamp = 
      oneOf
        [ int 
            |> Json.Decode.map toFloat
        , float  ]
        |> Json.Decode.map Date.fromTime

    Also popular now: