public class JsonResult<A> {

    final Validation<String, A> validation;

    private JsonResult(Validation<String, A> validation) {
        this.validation = validation;
    }

    public static <A> JsonResult<A> fail(String msg) {
        return new JsonResult<>(Validation.fail(msg));
    }

    public static <A> JsonResult<A> success(A a) {
        return new JsonResult<>(Validation.success(a));
    }

    public static <A> JsonResult<A> tried(F0<A> a) {
        try {
            return new JsonResult<>(Validation.success(a.f()));
        } catch (Exception e) {
            return JsonResult.fail(e.getClass().getSimpleName() + ":" + e.getMessage());
        }
    }

    public <B> JsonResult<B> decode(JsonDecoder<B> decoder) {
        return onJsonValue(decoder);
    }

    public static <A> JsonResult<A> fromValidation(Validation<String, A> validation) {
        return new JsonResult<>(validation);
    }

    public JsonResult<JsonValue> field(String field) {
        return onJsonValue(json ->
          json.field(field));
    }

    public boolean containsField(String field){
        return onJsonValue(jsonValue -> JsonResult.success(jsonValue.onObject(fields -> fields.contains(field)).orSome(false))).orElse(()->false);
    }

    public JsonResult<JsonValue> index(int i) {
        return asArray().bind(list -> JsonResult.tried(() -> (list.toArray().get(i))));
    }

    public ArrayResult<JsonValue> asArray() {
        return new ArrayResult<>(onJsonValue(jsonValue -> jsonValue.onArray(JsonResult::success).orSome(JsonResult.fail("Not an array"))).validation);
    }

    public JsonResult<JsonObject> asObject(){
        return onJsonValue(JsonValue::asObject);
    }

    public ArrayResult<JsonValue> fieldAsArray(String field) {
        return field(field).asArray();
    }

    public JsonResult<JsonObject> fieldAsObject(String field){
        return field(field).asObject();
    }

    public JsonResult<List<String>> asStrings() {
        return asArray().bind(list -> sequence(list.map(JsonValue::asText)));
    }

    public JsonResult<List<String>> fieldAsStrings(String field) {
        return field(field).asStrings();
    }

    public JsonResult<List<BigDecimal>> asNumbers() {
        return asArray().bind(list -> sequence(list.map(JsonValue::asNumber)));
    }

    public JsonResult<List<BigDecimal>> fieldAsNumbers(String field) {
        return field(field).asNumbers();
    }

    public JsonResult<List<Boolean>> asBools() {
        return asArray().bind(list -> sequence(list.map(JsonValue::asBool)));
    }

    public JsonResult<List<Boolean>> fieldAsBools(String field) {
        return field(field).asBools();
    }

    public JsonResult<String> indexAsString(int i) {
        return index(i).asString();
    }

    public JsonResult<BigDecimal> indexAsNumber(int i) {
        return index(i).asNumber();
    }

    public JsonResult<Boolean> indexAsBool(int i) {
        return index(i).asBoolean();
    }

    public String indexAsString(int i, String defaultValue) {
        return indexAsString(i).orElse(() -> defaultValue);
    }

    public BigDecimal indexAsNumber(int i, BigDecimal defaultValue) {
        return indexAsNumber(i).orElse(() -> defaultValue);
    }

    public Boolean indexAsBool(int i, Boolean defaultValue) {
        return indexAsBool(i).orElse(() -> defaultValue);
    }

    public JsonResult<String> fieldAsString(String field) {
        return field(field).asString();
    }

    public JsonResult<BigDecimal> fieldAsNumber(String field) {
        return field(field).asNumber();
    }

    public JsonResult<Boolean> fieldAsBool(String field) {
        return field(field).asBoolean();
    }

    public String fieldAsString(String field, String defaultValue) {
        return field(field).asString().orElse(() -> defaultValue);
    }

    public JsonResult<String> asString() {
        return onJsonValue(JsonValue::asText);
    }

    public JsonResult<BigDecimal> asNumber() {
        return onJsonValue(JsonValue::asNumber);
    }

    public JsonResult<Long> asLong() {
        return onJsonValue(JsonValue::asLong);
    }

    public JsonResult<Boolean> asBoolean() {
        return onJsonValue(JsonValue::asBool);
    }

    public String asStringE() {
        return asString().orThrow(JsonConversionFailure::new);
    }

    public BigDecimal asNumberE() {
        return asNumber().orThrow(JsonConversionFailure::new);
    }

    public Boolean asBooleanE() {
        return asBoolean().orThrow(JsonConversionFailure::new);
    }


    public String asString(String defaultValue) {
        return asString().orElse(() -> defaultValue);
    }

    public BigDecimal asNumber(BigDecimal defaultNumber){
        return asNumber().orElse(()->defaultNumber);
    }

    public Boolean asBoolean(boolean defaultBoolean){
        return asBoolean().orElse(()->defaultBoolean);
    }


    public <A> JsonResult<A> onJsonValue(F<JsonValue, JsonResult<A>> f) {
        return validation.validation(
          fail -> JsonResult.fail(fail),
          s -> {
              if (s instanceof JsonValue) {
                  return f.f((JsonValue) s);
              } else
                  return JsonResult.fail("Not a json value");
          }
        );
    }

    /**
     * Returns a validation that either contains a value or a failmessage.
     * @return a validation.
     */
    public Validation<String, A> toValidation() {
        return validation;
    }

    /**
     * Returns Some(a) if the JsonResult contains a value, None otherwise.
     * @return An Option
     */
    public Option<A> toOption() {
        return toValidation().toOption();
    }

    public static <A> JsonResult<List<A>> sequence(List<JsonResult<A>> results) {
        return results.foldLeft(
          (memo, result) -> result.bind(a -> memo.map(list -> list.cons(a))),
          JsonResult.success(List.<A>nil())).map(List::reverse);
    }

    public <B> JsonResult<B> mod(F<Validation<String, A>, Validation<String, B>> f) {
        try {
            return new JsonResult<>(f.f(validation));
        }catch (Exception e){
            return new JsonResult<>(Validation.fail("Failed to transform "+toString()+": "+e.getClass().getSimpleName()+"-"+e.getMessage()));
        }
    }

    public <T> T fold(F<String, T> onError, F<A, T> onSuccess) {
        return validation.validation(onError, onSuccess);
    }

    public JsonResult<A> mapFail(F<String, String> f) {
        return mod(validation -> validation.f().map(f));
    }

    public <B> JsonResult<B> map(F<A, B> f) {
        return mod(v -> v.map(f));
    }

    public <B> JsonResult<B> bind(F<A, JsonResult<B>> f) {
        return mod(v -> v.map(f).bind(v2 -> v2.validation));
    }

    public A orElse(F0<A> a) {
        return validation.validation(f -> a.f(), aa -> aa);
    }

    public A orElse(F<String, A> a) {
        return validation.validation(a::f, aa -> aa);
    }

    public JsonResult<A> orResult(F0<JsonResult<A>> other) {
        return bind(u -> other.f());
    }

    public A orThrow() {
        return orThrow(RuntimeException::new);
    }

    public A orThrow(F<String, ? extends RuntimeException> supplier) {
        if (validation.isFail())
            throw supplier.f(validation.fail());
        else return validation.success();
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("JsonResult{");
        sb.append(validation.validation(f -> f, Object::toString));
        sb.append('}');
        return sb.toString();
    }

    public static class ArrayResult<A> extends JsonResult<List<A>> {

        public ArrayResult(Validation<String, List<A>> validation) {
            super(validation);
        }

        public <B> ArrayResult<B> mapArray(F<A, B> f) {
            return new ArrayResult<B>(validation.map(list -> list.map(f)));
        }

        public <B> ArrayResult<B> mapFlattenArray(F<A, JsonResult<B>> f) {
            return new ArrayResult<B>(validation.bind(list -> JsonResult.sequence(list.map(f)).validation));

        }
    }
} public interface JsonDecoder<A> extends F<JsonValue, JsonResult<A>> {

    default JsonResult<A> f(JsonValue v) {
        return decode(v);
    }

    /**
     * Decodes the value into a domain object.
     *
     * @param v The jsonvalue to convert
     * @return The conversion result.
     */
    JsonResult<A> decode(JsonValue v);

    /**
     * Adapt the output of this codec into a value in your domain.
     *
     * @param f   The lambda that adapts the original output of this decoder.
     * @param <B> The tyoe of the value your are adapting to
     * @return A decoder that decodes the jsonvalue into your value.
     */
    default <B> JsonDecoder<B> map(F<A, B> f) {
        return v -> this.decode(v).map(f);
    }

    /**
     * When converting from json to your domain model, every conversion might fail.
     * To short circuit the decodingprocess when a decoder fails you can bind them together using
     * this method. The lambda you insert as an argument will receive the result of the first
     * decoder if it succeeds, and then you can supply the second decoder.
     * If the first one fails, the second one is never called.
     * <p>
     * This method is frequently called 'andThen' because it runs the first decoder andThen the
     * second decoder id the first one succeeds.
     *
     * @param f   The lambda that supplies the second decoder
     * @param <B> the type the second decoder converts to
     * @return a new decoder that binds this decoder with the second decoder.
     */
    default <B> JsonDecoder<B> bind(F<A, JsonDecoder<B>> f) {
        return v -> this.decode(v).bind(a -> f.f(a).decode(v));
    }

    /**
     * When decoding values it is sometimes useful to add constraints to
     * the values, forcing the decoder to fail if some requirements arent met.
     *
     * @param pred Test the decoded value.
     * @return A decoder that fails if the predicate is not met.
     */
    default JsonDecoder<A> ensure(F<A, Boolean> pred) {
        return ensure(pred, pred.toString());
    }

    /**
     * As ensure(pred), but with a simple message.
     *
     * @param pred The predicate that must hold
     * @param msg Message to output if fail
     * @return A new decoder that fails if the predicate does not hold
     */
    default JsonDecoder<A> ensure(F<A, Boolean> pred, String msg) {
        return ensure(pred,decoded -> "The value " + decoded + " did not satisfy the constraint " + msg);
    }

    /**
     * As ensure(pred), but you can supply the error message yourself
     *
     * @param pred The predicate that must yield true
     * @param msg The message to outputif the predicate does not hold
     * @return A new decoder that fails if the predicate does not hold.
     */
    default JsonDecoder<A> ensure(F<A, Boolean> pred, F<A, String> msg) {
        return v ->
            this.decode(v).bind(decoded ->
                pred.f(decoded) ?
                JsonResult.success(decoded) :
                JsonResult.fail(msg.f(decoded)));
    }
} public class JsonDecoders {

    public static final JsonDecoder<String> stringDecoder =
        JsonValue::asText;

    public static final JsonDecoder<BigDecimal> bigDecimalDecoder =
        JsonValue::asNumber;

    public static final JsonDecoder<Integer> intDecoder =
        bigDecimalDecoder.map(BigDecimal::intValue);

    public static final JsonDecoder<Boolean> boolDecoder =
        JsonValue::asBool;

    public static <A> JsonDecoder<Option<A>> optionDecoder(JsonDecoder<A> da) {
        return v ->
            v
                .onNull(() -> success(Option.<A>none()))
                .orSome(da.decode(v).map(Option::some));
    }

    public static <A> JsonDecoder<List<A>> arrayDecoder(JsonDecoder<A> ad) {
        return v ->
            v.onArray(list -> sequence(list.map(ad))).orSome(fail(v.toString() + " does not represent an array"));
    }

    public static <A> JsonDecoder<A> arrayIndexDecoder(int i, JsonDecoder<A> ad) {
        return v ->
            v
                .onArray(list -> tried(() -> ad.decode(list.toArray().get(i))).bind(x -> x))
                .orSome(fail(v.toString() + " does not represent an array"));
    }

    public static <A> JsonDecoder<TreeMap<String, A>> fieldsDecoder(JsonDecoder<A> aDecoder) {
        return v ->
            v.onObject(props ->
                sequence(
                    props
                        .toList()
                        .map(p2 -> aDecoder.decode(p2._2()).map(a -> P.p(p2._1(), a)))
                ).map(list -> TreeMap.iterableTreeMap(Ord.stringOrd, list)))
                .orSome(fail(v + "is not an object"));
    }

    public static <A> FieldDecoder<A> field(String name, JsonDecoder<A> valueDecoder) {
        return obj ->
            obj.get(name)
                .option(
                    fail("No field with name " + name),
                    v ->
                        valueDecoder
                            .decode(v)
                            .mapFail(str -> "Failure while decoding field " + name + ": " + str));
    }

    public static <A> FieldDecoder<Option<A>> optionalField(String name, JsonDecoder<A> valueDecoder) {
        return obj ->
            obj.get(name)
                .option(
                    success(Option.<A>none()),
                    v -> {
                        boolean isNull =
                            v.onNull(() -> true).isSome();

                        return
                            isNull ?
                            success(Option.none()) :
                            valueDecoder
                                .decode(v)
                                .map(Option::some)
                                .mapFail(str -> "Failure while decoding field " + name + ": " + str);
                    });
    }

    public static <A> JsonDecoder<A> obj(FieldDecoder<A> ad) {
        return v ->
            v.onObject(ad::apply).orSome(notAnObjectFailMsg(v));
    }

    public static <A, B> JsonDecoder<P2<A, B>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b) {
        return v ->
            v.onObject(pair(a, b)::apply).orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C> JsonDecoder<C> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        F2<A, B, C> f) {
        return obj(a, b).map(t -> f.f(t._1(), t._2()));
    }

    public static <A, B, C> JsonDecoder<P3<A, B, C>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, c)).apply(obj).map(Products::flatten3))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D> JsonDecoder<D> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        F3<A, B, C, D> f) {
        return obj(a, b, c).map(t -> f.f(t._1(), t._2(), t._3()));
    }

    public static <A, B, C, D> JsonDecoder<P4<A, B, C, D>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, d))).apply(obj).map(Products::flatten4))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E> JsonDecoder<E> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        F4<A, B, C, D, E> f) {
        return obj(a, b, c, d).map(t -> f.f(t._1(), t._2(), t._3(), t._4()));
    }

    public static <A, B, C, D, E> JsonDecoder<P5<A, B, C, D, E>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, pair(d, e)))).apply(obj).map(Products::flatten5))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E, FF> JsonDecoder<FF> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        F5<A, B, C, D, E, FF> f) {
        return obj(a, b, c, d, e).map(t -> f.f(t._1(), t._2(), t._3(), t._4(), t._5()));
    }

    public static <A, B, C, D, E, FF> JsonDecoder<P6<A, B, C, D, E, FF>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> f) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, pair(d, pair(e, f))))).apply(obj).map(Products::flatten6))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E, FF, G> JsonDecoder<G> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> ff,
        F6<A, B, C, D, E, FF, G> f) {
        return obj(a, b, c, d, e, ff).map(t -> f.f(t._1(), t._2(), t._3(), t._4(), t._5(), t._6()));
    }

    public static <A, B, C, D, E, FF, G> JsonDecoder<P7<A, B, C, D, E, FF, G>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> f,
        FieldDecoder<G> g) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, pair(d, pair(e, pair(f, g)))))).apply(obj).map(Products::flatten7))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E, FF, G, H> JsonDecoder<H> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> ff,
        FieldDecoder<G> g,
        F7<A, B, C, D, E, FF, G, H> f) {
        return obj(a, b, c, d, e, ff, g).map(t -> f.f(t._1(), t._2(), t._3(), t._4(), t._5(), t._6(), t._7()));
    }

    public static <A, B, C, D, E, FF, G, H> JsonDecoder<P8<A, B, C, D, E, FF, G, H>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> f,
        FieldDecoder<G> g,
        FieldDecoder<H> h) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, pair(d, pair(e, pair(f, pair(g, h))))))).apply(obj).map(Products::flatten8))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E, FF, G, H, I> JsonDecoder<I> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> ff,
        FieldDecoder<G> g,
        FieldDecoder<H> h,
        F8<A, B, C, D, E, FF, G, H, I> f) {
        return obj(a, b, c, d, e, ff, g, h).map(t -> f.f(t._1(), t._2(), t._3(), t._4(), t._5(), t._6(), t._7(), t._8()));
    }

    public static <A> JsonDecoder<A> objE(Try1<JsonResult<JsonObject>, A, JsonConversionFailure> f) {
        return value -> {
            JsonResult<JsonObject> jobj =
                value
                    .onObject(map -> success(new JsonObject(map)))
                    .orSome(fail(value + " is not a json object"));
            try {
                return success(f.f(jobj));
            } catch (JsonConversionFailure e) {
                return fail(e.getMessage());
            }
        };
    }

    public static <A> JsonDecoder<A> subclassObjDecoder(
        String selectorFieldName,
        P2<String, JsonDecoder<A>>... decoders) {
        List<P2<String, JsonDecoder<A>>> decoderList = List.arrayList(decoders);
        return v ->
            v.fieldAsText(selectorFieldName).bind(type ->
                decoderList
                    .find(pair -> pair._1().equals(type))
                    .option(
                        fail("No decoder was registererd for type " + type),
                        found -> found._2().decode(v)
                    )
            );
    }

    public interface FieldDecoder<A> {
        JsonResult<A> apply(TreeMap<String, JsonValue> fields);

        default <B> FieldDecoder<B> map(F<A, B> f) {
            return fields -> apply(fields).map(f);
        }

        default FieldDecoder<A> or(FieldDecoder<A> other) {
            return fields -> apply(fields).orResult(() -> other.apply(fields));
        }
    }

    private static <A, B> FieldDecoder<P2<A, B>> pair(FieldDecoder<A> ad, FieldDecoder<B> bd) {
        return fields -> ad.apply(fields).bind(a -> bd.apply(fields).map(b -> p(a, b)));
    }

    public static <A, B> JsonDecoder<P2<A, B>> and(JsonDecoder<A> aDecoder, JsonDecoder<B> bDecoder) {
        return v -> aDecoder.decode(v).bind(a -> bDecoder.decode(v).map(b -> p(a, b)));
    }

    public static <A, B, C> JsonDecoder<C> and(JsonDecoder<A> aDecoder, JsonDecoder<B> bDecoder, F2<A, B, C> join) {
        return and(aDecoder, bDecoder).map(t -> join.f(t._1(), t._2()));
    }

    private static <A> JsonResult<A> notAnObjectFailMsg(JsonValue v) {
        return fail("Tried to decode an object but i got a" + v.toString() + ". Did you use the right decoder?");
    }
} public interface JsonEncoder<A> extends F<A, JsonValue> {

    JsonValue encode(A t);

    /**
     * Convert an object of type A to a JsonValue
     * @param a The value to convert
     * @return The value repesented in json.
     */
    @Override default JsonValue f(A a) {
        return encode(a);
    }

    /**
     * Adapt the type of the input so it fits into this codec.
     * @param f The lambda that adapts your domain value into a value this encoder can encode
     * @param <B> The type of the value you are adapting
     * @return an encoder that can encode your domain values.
     */
    default <B> JsonEncoder<B> contramap(F<B, A> f) {
        return b -> this.encode(f.f(b));
    }

} public class JsonEncoders {

    /**
     * Encoder that encodes strings into json strings
     */
    public static final JsonEncoder<String> stringEncoder =
        JsonValues::jString;

    /**
     * Encoder that encodes bigdecimals into json numbers
     */
    public static final JsonEncoder<BigDecimal> bigDecimalEncoder =
        JsonValues::jNum;

    /**
     * Encoder that encodes boolean values
     */
    public static final JsonEncoder<Boolean> boolEncoder =
        JsonValues::jBool;

    /**
     * Encoder that encodes integers.
     */
    public static final JsonEncoder<Integer> integerEncoder =
        bigDecimalEncoder.contramap(BigDecimal::valueOf);

    /**
     * Encoder that encodes optional values.
     * @param aEncoder The encoder that can encode the contents of the optional value
     * @param <A> the type of the optional value
     * @return an encoder that can encode optional values.
     */
    public static <A> JsonEncoder<Option<A>> optionEncoder(JsonEncoder<A> aEncoder) {
        return maybeValue -> maybeValue.option(JsonValues.jNull(), aEncoder::encode);
    }

    /**
     * Encodes a fj.data.List into a json array
     *
     * @param aEncoder the encoder for the elements in the list
     * @param <A> the type of the elements
     * @return an encoder that encodes lists
     */
    public static <A> JsonEncoder<List<A>> arrayEncoder(JsonEncoder<A> aEncoder) {
        return list -> JsonValues.jArray(list.map(aEncoder::encode));
    }

    /**
     * Encodes a jf.data.TreeMap into a json object where the keys in the map are
     * represented as field names, and the values are encoded using the supplied encoder.
     *
     * @param aEncoder The encoder that encodes the values in the map
     * @param <A>      The type of the values in the map
     * @return an encoder that can encode maps
     */
    public static <A> JsonEncoder<TreeMap<String, A>> fieldsEncoder(JsonEncoder<A> aEncoder) {
        return map -> JsonValues.jObj(map.map(aEncoder::encode));
    }

    /**
     * A special encoder that is used when constructing objects. (see obj)
     *
     * @param name The name of the field
     * @param a    The encoder that encodes the value
     * @param <A>  The type of the value
     * @return a special encoder that help construct objects.
     */
    public static <A> FieldEncoder<A> field(String name, JsonEncoder<A> a) {
        return (obj, va) -> obj.withField(name, a.encode(va));
    }

    /**
     * Encodes an domain value into a json object.
     *
     * @param fe  the fieldencoder for that field.
     * @param <A> The type of the value that is encoded as the field value
     * @return an encoder that creates json objects
     */
    public static <A> JsonEncoder<A> obj(FieldEncoder<A> fe) {
        return a -> fe.apply(JsonObject.empty, a);
    }

    /**
     * En encoder that encodes domain values into a json object. You supply a field encoder that encodes a field, and
     * a lambda that converts your domain value into the type of the field converter.
     *
     * @param fe the encoder for the field
     * @param f unwraps the object
     * @param <A> the type of the field
     * @param <X> the type of the object
     * @return an encoder that encodes an object as a json obejct
     */
    public static <A, X> JsonEncoder<X> obj(FieldEncoder<A> fe, F<X, A> f) {
        return obj(fe).contramap(f);
    }

    /**
     * An encoder that encodes a tuple of domain values into a json object. You supply encoders for each field.
     *
     * @param a   The encoder that knows how to encode the first field value
     * @param b   The encoder that knows how to encode the second field value
     * @param <A> The type of the first field value
     * @param <B> The type of the second field value
     * @return an encoder that encodes tuples as a json object
     */
    public static <A, B> JsonEncoder<P2<A, B>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b) {
        return p ->
            and(a, b).apply(JsonObject.empty, p);
    }

    /**
     * An encoder that encodes domain object into a json object. You supply encoders for each field, and a lambda that extracts the values that
     *
     * @param a   The encoder that knows how to encode the first field value
     * @param b   The encoder that knows how to encode the second field value
     * @param <A> The type of the first field value
     * @param <B> The type of the second field value
     * @param <X> The type of the domain object
     * @return An encoder that encodes domain objects of type X into a json object.
     */
    public static <A, B, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        F<X, P2<A, B>> f) {
        return
            obj(a, b).contramap(f);
    }

    public static <A, B, C> JsonEncoder<P3<A, B, C>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c) {
        return t ->
            and(a, and(b, c)).apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        F<X, P3<A, B, C>> f) {
        return
            obj(a, b, c).contramap(f);
    }

    public static <A, B, C, D> JsonEncoder<P4<A, B, C, D>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d) {
        return t ->
            and(a, and(b, and(c, d)))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        F<X, P4<A, B, C, D>> f) {
        return obj(a, b, c, d).contramap(f);
    }

    public static <A, B, C, D, E> JsonEncoder<P5<A, B, C, D, E>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e) {
        return t ->
            and(a, and(b, and(c, and(d, e))))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, E, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        F<X, P5<A, B, C, D, E>> f) {
        return
            obj(a, b, c, d, e).contramap(f);
    }

    public static <A, B, C, D, E, FF> JsonEncoder<P6<A, B, C, D, E, FF>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> f) {
        return t ->
            and(a, and(b, and(c, and(d, and(e, f)))))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, E, FF, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> ff,
        F<X, P6<A, B, C, D, E, FF>> f) {
        return obj(a, b, c, d, e, ff).contramap(f);
    }

    public static <A, B, C, D, E, FF, G> JsonEncoder<P7<A, B, C, D, E, FF, G>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> f,
        FieldEncoder<G> g) {
        return t ->
            and(a, and(b, and(c, and(d, and(e, and(f, g))))))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, E, FF, G, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> ff,
        FieldEncoder<G> g,
        F<X, P7<A, B, C, D, E, FF, G>> f) {
        return obj(a, b, c, d, e, ff, g).contramap(f);
    }

    public static <A, B, C, D, E, FF, G, H> JsonEncoder<P8<A, B, C, D, E, FF, G, H>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> f,
        FieldEncoder<G> g,
        FieldEncoder<H> h) {
        return t ->
            and(a, and(b, and(c, and(d, and(e, and(f, and(g, h)))))))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, E, FF, G, H, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> ff,
        FieldEncoder<G> g,
        FieldEncoder<H> h,
        F<X, P8<A, B, C, D, E, FF, G, H>> f) {
        return obj(a, b, c, d, e, ff, g, h).contramap(f);
    }

    public interface FieldEncoder<A> {
        JsonObject apply(JsonObject obj, A a);
    }

    private static <A, B> FieldEncoder<P2<A, B>> and(FieldEncoder<A> fa, FieldEncoder<B> fb) {
        return (obj, t) -> fb.apply(fa.apply(obj, t._1()), t._2());
    }

} public class JsonParser {

    private static final int MIN_BUFFER_SIZE     = 10;
    private static final int DEFAULT_BUFFER_SIZE = 1024;

    private final Reader        reader;
    private final char[]        buffer;
    private       int           bufferOffset;
    private       int           index;
    private       int           fill;
    private       int           line;
    private       int           lineOffset;
    private       int           current;
    private       StringBuilder captureBuffer;
    private       int           captureStart;

  /*
   * |                      bufferOffset
   *                        v
   * [a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t]        < input
   *                       [l|m|n|o|p|q|r|s|t|?|?]    < buffer
   *                          ^               ^
   *                       |  index           fill
   */

    private JsonParser(String string) {
        this(new StringReader(string),
          Math.max(MIN_BUFFER_SIZE, Math.min(DEFAULT_BUFFER_SIZE, string.length())));
    }

    private JsonParser(Reader reader) {
        this(reader, DEFAULT_BUFFER_SIZE);
    }

    private JsonParser(Reader reader, int buffersize) {
        this.reader = reader;
        buffer = new char[buffersize];
        line = 1;
        captureStart = -1;
    }

    private JsonValue parse() throws IOException {
        read();
        skipWhiteSpace();
        JsonValue result = readValue();
        skipWhiteSpace();
        if (!isEndOfText()) {
            throw error("Unexpected character");
        }
        return result;
    }

    public static JsonResult<JsonValue> parse(String string) {
        try {
            return JsonResult.success(new JsonParser(string).parse());
        } catch (IOException ioe) {
            return JsonResult.fail("IOException while parsing: " + ioe.getMessage());
        } catch (ParseFailure f) {
            return JsonResult.fail("Failed to parse resource: " + f.getMessage() + ": line " + f.line + ", " + f.offset + ", i" + f.i);
        }
    }

    public static JsonResult<JsonValue> parse(Reader string) {
        try {
            return JsonResult.success(new JsonParser(string).parse());
        } catch (IOException ioe) {
            return JsonResult.fail("IOException while parsing: " + ioe.getMessage());
        } catch (ParseFailure f) {
            return JsonResult.fail("Failed to parse resource: " + f.getMessage() + ": line " + f.line + ", " + f.offset + ", i" + f.i);
        }
    }

    private JsonValue readValue() throws IOException {
        switch (current) {
            case 'n':
                return readNull();
            case 't':
                return readTrue();
            case 'f':
                return readFalse();
            case '"':
                return readString();
            case '[':
                return readArray();
            case '{':
                return readObject();
            case '-':
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                return readNumber();
            default:
                throw expected("value");
        }
    }

    private JsonArray readArray() throws IOException {
        read();
        skipWhiteSpace();
        if (readChar(']')) {
            return new JsonArray(List.nil());
        }
        List<JsonValue> list = List.nil();
        do {
            skipWhiteSpace();
            list = list.cons(readValue());
            skipWhiteSpace();
        } while (readChar(','));
        if (!readChar(']')) {
            throw expected("',' or ']'");
        }
        return new JsonArray(list.reverse());
    }

    private JsonObject readObject() throws IOException {
        read();
        skipWhiteSpace();
        if (readChar('}')) {
            return new JsonObject(TreeMap.empty(Ord.stringOrd));
        }
        TreeMap<String, JsonValue> contents = TreeMap.empty(Ord.stringOrd);
        do {
            skipWhiteSpace();
            String name = readName();
            skipWhiteSpace();
            if (!readChar(':')) {
                throw expected("':'");
            }
            skipWhiteSpace();
            contents = contents.set(name, readValue());
            skipWhiteSpace();
        } while (readChar(','));
        if (!readChar('}')) {
            throw expected("',' or '}'");
        }
        return new JsonObject(contents);
    }

    private String readName() throws IOException {
        if (current != '"') {
            throw expected("name");
        }
        return readStringInternal();
    }

    private JsonValue readNull() throws IOException {
        read();
        readRequiredChar('u');
        readRequiredChar('l');
        readRequiredChar('l');
        return jNull();
    }

    private JsonValue readTrue() throws IOException {
        read();
        readRequiredChar('r');
        readRequiredChar('u');
        readRequiredChar('e');
        return jBool(true);
    }

    private JsonValue readFalse() throws IOException {
        read();
        readRequiredChar('a');
        readRequiredChar('l');
        readRequiredChar('s');
        readRequiredChar('e');
        return jBool(false);
    }

    private void readRequiredChar(char ch) throws IOException {
        if (!readChar(ch)) {
            throw expected("'" + ch + "'");
        }
    }

    private JsonValue readString() throws IOException {
        return jString(readStringInternal());
    }

    private String readStringInternal() throws IOException {
        read();
        startCapture();
        while (current != '"') {
            if (current == '\\') {
                pauseCapture();
                readEscape();
                startCapture();
            } else if (current < 0x20) {
                throw expected("valid string character");
            } else {
                read();
            }
        }
        String string = endCapture();
        read();
        return string;
    }

    private void readEscape() throws IOException {
        read();
        switch (current) {
            case '"':
            case '/':
            case '\\':
                captureBuffer.append((char) current);
                break;
            case 'b':
                captureBuffer.append('\b');
                break;
            case 'f':
                captureBuffer.append('\f');
                break;
            case 'n':
                captureBuffer.append('\n');
                break;
            case 'r':
                captureBuffer.append('\r');
                break;
            case 't':
                captureBuffer.append('\t');
                break;
            case 'u':
                char[] hexChars = new char[4];
                for (int i = 0; i < 4; i++) {
                    read();
                    if (!isHexDigit()) {
                        throw expected("hexadecimal digit");
                    }
                    hexChars[i] = (char) current;
                }
                captureBuffer.append((char) Integer.parseInt(new String(hexChars), 16));
                break;
            default:
                throw expected("valid escape sequence");
        }
        read();
    }

    private JsonValue readNumber() throws IOException {
        startCapture();
        readChar('-');
        int firstDigit = current;
        if (!readDigit()) {
            throw expected("digit");
        }
        if (firstDigit != '0') {
            while (readDigit()) {
            }
        }
        readFraction();
        readExponent();
        return jNum(new BigDecimal(endCapture()));
    }

    private boolean readFraction() throws IOException {
        if (!readChar('.')) {
            return false;
        }
        if (!readDigit()) {
            throw expected("digit");
        }
        while (readDigit()) {
        }
        return true;
    }

    private boolean readExponent() throws IOException {
        if (!readChar('e') && !readChar('E')) {
            return false;
        }
        if (!readChar('+')) {
            readChar('-');
        }
        if (!readDigit()) {
            throw expected("digit");
        }
        while (readDigit()) {
        }
        return true;
    }

    private boolean readChar(char ch) throws IOException {
        if (current != ch) {
            return false;
        }
        read();
        return true;
    }

    private boolean readDigit() throws IOException {
        if (!isDigit()) {
            return false;
        }
        read();
        return true;
    }

    private void skipWhiteSpace() throws IOException {
        while (isWhiteSpace()) {
            read();
        }
    }

    private void read() throws IOException {
        if (index == fill) {
            if (captureStart != -1) {
                captureBuffer.append(buffer, captureStart, fill - captureStart);
                captureStart = 0;
            }
            bufferOffset += fill;
            fill = reader.read(buffer, 0, buffer.length);
            index = 0;
            if (fill == -1) {
                current = -1;
                return;
            }
        }
        if (current == '\n') {
            line++;
            lineOffset = bufferOffset + index;
        }
        current = buffer[index++];
    }

    private void startCapture() {
        if (captureBuffer == null) {
            captureBuffer = new StringBuilder();
        }
        captureStart = index - 1;
    }

    private void pauseCapture() {
        int end = current == -1 ? index : index - 1;
        captureBuffer.append(buffer, captureStart, end - captureStart);
        captureStart = -1;
    }

    private String endCapture() {
        int    end = current == -1 ? index : index - 1;
        String captured;
        if (captureBuffer.length() > 0) {
            captureBuffer.append(buffer, captureStart, end - captureStart);
            captured = captureBuffer.toString();
            captureBuffer.setLength(0);
        } else {
            captured = new String(buffer, captureStart, end - captureStart);
        }
        captureStart = -1;
        return captured;
    }

    private ParseFailure expected(String expected) {
        if (isEndOfText()) {
            return error("Unexpected end of input");
        }
        return error("Expected " + expected);
    }

    private ParseFailure error(String message) {
        int absIndex = bufferOffset + index;
        int column   = absIndex - lineOffset;
        int offset   = isEndOfText() ? absIndex : absIndex - 1;
        return new ParseFailure(message, offset, line, column - 1);
    }

    private boolean isWhiteSpace() {
        return current == ' ' || current == '\t' || current == '\n' || current == '\r';
    }

    private boolean isDigit() {
        return current >= '0' && current <= '9';
    }

    private boolean isHexDigit() {
        return current >= '0' && current <= '9'
          || current >= 'a' && current <= 'f'
          || current >= 'A' && current <= 'F';
    }

    private boolean isEndOfText() {
        return current == -1;
    }

} public class JsonWriter {
    private static final int CONTROL_CHARACTERS_END = 0x001f;

    private static final char[] QUOT_CHARS         = {'\\', '"'};
    private static final char[] BS_CHARS           = {'\\', '\\'};
    private static final char[] LF_CHARS           = {'\\', 'n'};
    private static final char[] CR_CHARS           = {'\\', 'r'};
    private static final char[] TAB_CHARS          = {'\\', 't'};
    // In JavaScript, U+2028 and U+2029 characters count as line endings and must be encoded.
    // http://stackoverflow.com/questions/2965293/javascript-parse-error-on-u2028-unicode-character
    private static final char[] UNICODE_2028_CHARS = {'\\', 'u', '2', '0', '2', '8'};
    private static final char[] UNICODE_2029_CHARS = {'\\', 'u', '2', '0', '2', '9'};
    private static final char[] HEX_DIGITS         = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
      'a', 'b', 'c', 'd', 'e', 'f'};


    public static String write(JsonValue json) {
        return json
          .onNull(() -> "null")
          .orElse(json.onBool(Object::toString))
          .orElse(json.onNumber(BigDecimal::toString))
          .orElse(json.onString(s -> "\"" + writeJsonString(s) + "\""))
          .orElse(json.onArray(arr -> arr.isEmpty() ? "[]" : "[" + arr.tail().foldLeft((sum, val) -> sum + "," + write(val), write(arr.head())) + "]"))
          .orElse(json.onObject(obj -> mkString(obj.toList().map(pairs -> "\"" + pairs._1() + "\":" + write(pairs._2())), "{", ",", "}")))
          .orSome("");
    }

    public static String writePretty(JsonValue json) {
        return writePretty(json, 0);
    }

    private static String writePretty(JsonValue json, int indent) {
        return json
          .onNull(() -> "null")
          .orElse(json.onBool(Object::toString))
          .orElse(json.onNumber(BigDecimal::toString))
          .orElse(json.onString(s -> "\"" + writeJsonString(s) + "\""))
          .orElse(json.onArray(arr -> arr.isEmpty() ? "[]" : "[\n" + arr.tail().foldLeft((sum, val) -> sum + line(",") + indent(indent + 2) + writePretty(val, indent + 2), indent(indent + 2) + writePretty(arr.head(), indent + 2)) + "\n" + indent(indent) + "]"))
          .orElse(json.onObject(obj -> mkString(obj.toList().map(pairs -> indent(indent + 2) + "\"" + pairs._1() + "\":" + writePretty(pairs._2(), indent + 2)), line("{"), ",\n", "\n" + indent(indent) + "}")))
          .orSome("");
    }

    private static String mkString(List<String> vals, String pre, String delim, String post) {
        return vals.isEmpty()
          ? pre + post
          : pre + vals.tail().foldLeft((sum, v) -> sum + delim + v, vals.head()) + post;
    }

    private static String indent(int depth) {
        return Stream.asString(Stream.repeat(' ').take(depth));
    }

    private static String line(String l) {
        return l + "\n";
    }


    private static Effect1<Writer> write(TryEffect1<Writer, IOException> effect) {
        return writer -> {
            try {
                effect.f(writer);
            } catch (Exception e) {
                throw new RuntimeException("Could not write json to writer: " + e.getMessage(), e);
            }
        };
    }

    private static String writeJsonString(String string) {
        try {
            StringWriter writer = new StringWriter();
            int          length = string.length();
            int          start  = 0;
            for (int index = 0; index < length; index++) {
                char[] replacement = getReplacementChars(string.charAt(index));
                if (replacement != null) {
                    writer.write(string, start, index - start);
                    writer.write(replacement);
                    start = index + 1;
                }
            }
            writer.write(string, start, length - start);
            return writer.toString();
        } catch (Exception e) {
            throw new RuntimeException("Could not write string" + e.getMessage(), e);
        }
    }


    private static char[] getReplacementChars(char ch) {
        if (ch > '\\') {
            if (ch < '\u2028' || ch > '\u2029') {
                // The lower range contains 'a' .. 'z'. Only 2 checks required.
                return null;
            }
            return ch == '\u2028' ? UNICODE_2028_CHARS : UNICODE_2029_CHARS;
        }
        if (ch == '\\') {
            return BS_CHARS;
        }
        if (ch > '"') {
            // This range contains '0' .. '9' and 'A' .. 'Z'. Need 3 checks to get here.
            return null;
        }
        if (ch == '"') {
            return QUOT_CHARS;
        }
        if (ch > CONTROL_CHARACTERS_END) {
            return null;
        }
        if (ch == '\n') {
            return LF_CHARS;
        }
        if (ch == '\r') {
            return CR_CHARS;
        }
        if (ch == '\t') {
            return TAB_CHARS;
        }
        return new char[]{'\\', 'u', '0', '0', HEX_DIGITS[ch >> 4 & 0x000f], HEX_DIGITS[ch & 0x000f]};
    }
} public class SkinnyDecoders {


    public static <A, T> JsonDecoder<T> skinny(JsonDecoder<A> ad, F<A, T> convert) {
        return arrayDecoder(ad).map(convert);
    }

    public static <A, B, T> JsonDecoder<T> skinny(JsonDecoder<A> ad, JsonDecoder<B> bd, F2<A, B, T> convert) {
        return arrayDecoder(ad, bd).map(F2Functions.tuple(convert));
    }

    public static <A, B, C, T> JsonDecoder<T> skinny(
      JsonDecoder<A> ad,
      JsonDecoder<B> bd,
      JsonDecoder<C> cd,
      F3<A, B, C, T> convert) {
        return arrayDecoder(ad, bd, cd).map(t -> convert.f(t._1(), t._2(), t._3()));
    }

    public static <A, B, C, D, T> JsonDecoder<T> skinny(
      JsonDecoder<A> ad,
      JsonDecoder<B> bd,
      JsonDecoder<C> cd,
      JsonDecoder<D> dd,
      F4<A, B, C, D, T> convert) {
        return arrayDecoder(ad, bd, cd, dd).map(t -> convert.f(t._1(), t._2(), t._3(), t._4()));
    }

    public static <A, B, C, D, E, T> JsonDecoder<T> skinny(
      JsonDecoder<A> ad,
      JsonDecoder<B> bd,
      JsonDecoder<C> cd,
      JsonDecoder<D> dd,
      JsonDecoder<E> ed,
      F5<A, B, C, D, E, T> convert) {
        return arrayDecoder(ad, bd, cd, dd, ed).map(t -> convert.f(t._1(), t._2(), t._3(), t._4(), t._5()));
    }

    public static <A, B, C, D, E, FF, T> JsonDecoder<T> skinny(
      JsonDecoder<A> ad,
      JsonDecoder<B> bd,
      JsonDecoder<C> cd,
      JsonDecoder<D> dd,
      JsonDecoder<E> ed,
      JsonDecoder<FF> fd,
      F6<A, B, C, D, E, FF, T> convert) {
        return arrayDecoder(ad, bd, cd, dd, ed, fd).map(t -> convert.f(t._1(), t._2(), t._3(), t._4(), t._5(), t._6()));
    }


    public static <A> JsonDecoder<A> arrayDecoder(JsonDecoder<A> ad) {
        return v ->
          v.asArray()
            .bind(list -> expectedLength(list, 1))
            .bind(list -> ad.decode(list.head()));
    }

    public static <A, B> JsonDecoder<P2<A, B>> arrayDecoder(JsonDecoder<A> ad, JsonDecoder<B> bd) {
        return v ->
          v.asArray()
            .bind(list -> expectedLength(list, 2))
            .bind(list -> decode(list, ad, bd));
    }

    public static <A, B, C> JsonDecoder<P3<A, B, C>> arrayDecoder(
      JsonDecoder<A> ad,
      JsonDecoder<B> bd,
      JsonDecoder<C> cd) {
        return v ->
          arrayDecoder(ad, arrayDecoder(bd, cd)).decode(v)
            .map(Products::flatten3);
    }

    public static <A, B, C, D> JsonDecoder<P4<A, B, C, D>> arrayDecoder(
      JsonDecoder<A> ad,
      JsonDecoder<B> bd,
      JsonDecoder<C> cd,
      JsonDecoder<D> dd) {
        return v ->
          arrayDecoder(ad, arrayDecoder(bd, arrayDecoder(cd, dd))).decode(v)
            .map(Products::flatten4);
    }

    public static <A, B, C, D, E> JsonDecoder<P5<A, B, C, D, E>> arrayDecoder(
      JsonDecoder<A> ad,
      JsonDecoder<B> bd,
      JsonDecoder<C> cd,
      JsonDecoder<D> dd,
      JsonDecoder<E> ed) {
        return v ->
          arrayDecoder(ad, arrayDecoder(bd, arrayDecoder(cd, arrayDecoder(dd, ed)))).decode(v)
            .map(Products::flatten5);
    }

    public static <A, B, C, D, E, FF> JsonDecoder<P6<A, B, C, D, E, FF>> arrayDecoder(
      JsonDecoder<A> ad,
      JsonDecoder<B> bd,
      JsonDecoder<C> cd,
      JsonDecoder<D> dd,
      JsonDecoder<E> ed,
      JsonDecoder<FF> fd) {
        return v ->
          arrayDecoder(ad, arrayDecoder(bd, arrayDecoder(cd, arrayDecoder(dd, arrayDecoder(ed, fd))))).decode(v)
            .map(Products::flatten6);
    }


    public static <A, B> JsonResult<P2<A, B>> decode(List<JsonValue> list, JsonDecoder<A> ad, JsonDecoder<B> bd) {
        return ad.decode(list.head()).bind(a -> bd.decode(list.tail().head()).map(b -> P.p(a, b)));
    }

    public static JsonResult<List<JsonValue>> expectedLength(List<JsonValue> list, int expect) {
        return list.length() < expect ?
               JsonResult.fail("Tried to decode a list with length " + expect + ", but got a list with length " + list.length()) :
               JsonResult.success(list);
    }

    public static <A> JsonResult<A> fail(JsonValue v) {
        return JsonResult.fail("Expected a list, but got a " + v);
    }
} public class SkinnyDemux<A> implements JsonDecoder<A> {

    private final TreeMap<String, JsonDecoder<A>> decoders;

    private SkinnyDemux(TreeMap<String, JsonDecoder<A>> decoders) {
        this.decoders = decoders;
    }

    public static <A> SkinnyDemux<A> demuxer() {
        TreeMap<String, JsonDecoder<A>> decoders =
          TreeMap.empty(Ord.stringOrd);
        return new SkinnyDemux<>(decoders);
    }

    public SkinnyDemux<A> add(String name, JsonDecoder<? extends A> decoder) {
        return new SkinnyDemux<>(decoders.set(name, (JsonDecoder<A>) decoder));
    }

    public JsonResult<A> decode(JsonValue value) {
        return
          value.asObject().bind(
            obj ->
              obj.pairs.min().map(pair -> {
                  String                 name         = pair._1();
                  Option<JsonDecoder<A>> maybeDecoder = decoders.get(name);
                  return maybeDecoder.map(decoder -> decoder.<A>decode(pair._2()))
                    .orSome(JsonResult.fail("No decoder registered with name " + name + " in demuxer"));
              }).orSome(JsonResult.fail("Found an empty object when trying to demux a message"))
          );
    }


} public class SkinnyEncoders {


    public static <A, T> JsonEncoder<T> skinnyEncoder(JsonEncoder<A> ae, F<T, A> extractor) {
        return t -> jArray(ae.encode(extractor.f(t)));
    }

    public static <A, B, T> JsonEncoder<T> skinnyEncoder(
      JsonEncoder<A> ae,
      JsonEncoder<B> be,
      F<T, P2<A, B>> extractor) {
        return t -> {
            P2<A, B> p = extractor.f(t);
            return jArray(ae.encode(p._1()), be.encode(p._2()));
        };
    }

    public static <A, B, C, T> JsonEncoder<T> skinnyEncoder(
      JsonEncoder<A> ae,
      JsonEncoder<B> be,
      JsonEncoder<C> ce,
      F<T, P3<A, B, C>> extractor) {
        return t -> {
            P3<A, B, C> p = extractor.f(t);
            return jArray(ae.encode(p._1()), be.encode(p._2()), ce.encode(p._3()));
        };
    }

    public static <A, B, C, D, T> JsonEncoder<T> skinnyEncoder(
      JsonEncoder<A> ae,
      JsonEncoder<B> be,
      JsonEncoder<C> ce,
      JsonEncoder<D> de,
      F<T, P4<A, B, C, D>> extractor) {
        return t -> {
            P4<A, B, C, D> p = extractor.f(t);
            return jArray(ae.encode(p._1()), be.encode(p._2()), ce.encode(p._3()), de.encode(p._4()));
        };
    }

    public static <A, B, C, D, E, T> JsonEncoder<T> skinnyEncoder(
      JsonEncoder<A> ae,
      JsonEncoder<B> be,
      JsonEncoder<C> ce,
      JsonEncoder<D> de,
      JsonEncoder<E> ee,
      F<T, P5<A, B, C, D, E>> extractor) {
        return t -> {
            P5<A, B, C, D, E> p = extractor.f(t);
            return jArray(ae.encode(p._1()), be.encode(p._2()), ce.encode(p._3()), de.encode(p._4()), ee.encode(p._5()));
        };
    }

    public static <A, B, C, D, E, FF, T> JsonEncoder<T> skinnyEncoder(
      JsonEncoder<A> ae,
      JsonEncoder<B> be,
      JsonEncoder<C> ce,
      JsonEncoder<D> de,
      JsonEncoder<E> ee,
      JsonEncoder<FF> fe,
      F<T, P6<A, B, C, D, E, FF>> extractor) {
        return t -> {
            P6<A, B, C, D, E, FF> p = extractor.f(t);
            return jArray(ae.encode(p._1()), be.encode(p._2()), ce.encode(p._3()), de.encode(p._4()), ee.encode(p._5()), fe.encode(p._6()));
        };
    }

} public class SkinnyMux<A> implements JsonEncoder<A> {

    private final F<A, Option<JsonValue>> encoders;

    public SkinnyMux(F<A, Option<JsonValue>> f) {
        this.encoders = f;
    }

    public static <A> SkinnyMux<A> muxer() {
        return new SkinnyMux<>(a -> Option.none());
    }

    public <T extends A> SkinnyMux<A> add(Class<T> c, String name, JsonEncoder<T> encoder) {
        F<A, Option<JsonValue>> f =
          a ->
            c.isInstance(a) ?
            Option.some(jObj(p(name, encoder.encode(c.cast(a))))) :
            Option.none();

        return new SkinnyMux<>(a -> encoders.f(a).orElse(f.f(a)));
    }

    @Override
    public JsonValue encode(A t) {
        return encoders.f(t).orSome(JsonValues.jNull());
    }


} public class JsonDecoders {

    public static final JsonDecoder<String> stringDecoder =
        JsonValue::asText;

    public static final JsonDecoder<BigDecimal> bigDecimalDecoder =
        JsonValue::asNumber;

    public static final JsonDecoder<Integer> intDecoder =
        bigDecimalDecoder.map(BigDecimal::intValue);

    public static final JsonDecoder<Boolean> boolDecoder =
        JsonValue::asBool;

    public static <A> JsonDecoder<Option<A>> optionDecoder(JsonDecoder<A> da) {
        return v ->
            v
                .onNull(() -> success(Option.<A>none()))
                .orSome(da.decode(v).map(Option::some));
    }

    public static <A> JsonDecoder<List<A>> arrayDecoder(JsonDecoder<A> ad) {
        return v ->
            v.onArray(list -> sequence(list.map(ad))).orSome(fail(v.toString() + " does not represent an array"));
    }

    public static <A> JsonDecoder<A> arrayIndexDecoder(int i, JsonDecoder<A> ad) {
        return v ->
            v
                .onArray(list -> tried(() -> ad.decode(list.toArray().get(i))).bind(x -> x))
                .orSome(fail(v.toString() + " does not represent an array"));
    }

    public static <A> JsonDecoder<TreeMap<String, A>> fieldsDecoder(JsonDecoder<A> aDecoder) {
        return v ->
            v.onObject(props ->
                sequence(
                    props
                        .toList()
                        .map(p2 -> aDecoder.decode(p2._2()).map(a -> P.p(p2._1(), a)))
                ).map(list -> TreeMap.iterableTreeMap(Ord.stringOrd, list)))
                .orSome(fail(v + "is not an object"));
    }

    public static <A> FieldDecoder<A> field(String name, JsonDecoder<A> valueDecoder) {
        return obj ->
            obj.get(name)
                .option(
                    fail("No field with name " + name),
                    v ->
                        valueDecoder
                            .decode(v)
                            .mapFail(str -> "Failure while decoding field " + name + ": " + str));
    }

    public static <A> FieldDecoder<Option<A>> optionalField(String name, JsonDecoder<A> valueDecoder) {
        return obj ->
            obj.get(name)
                .option(
                    success(Option.<A>none()),
                    v -> {
                        boolean isNull =
                            v.onNull(() -> true).isSome();

                        return
                            isNull ?
                            success(Option.none()) :
                            valueDecoder
                                .decode(v)
                                .map(Option::some)
                                .mapFail(str -> "Failure while decoding field " + name + ": " + str);
                    });
    }

    public static <A> JsonDecoder<A> obj(FieldDecoder<A> ad) {
        return v ->
            v.onObject(ad::apply).orSome(notAnObjectFailMsg(v));
    }

    public static <A, B> JsonDecoder<P2<A, B>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b) {
        return v ->
            v.onObject(pair(a, b)::apply).orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C> JsonDecoder<C> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        F2<A, B, C> f) {
        return obj(a, b).map(t -> f.f(t._1(), t._2()));
    }

    public static <A, B, C> JsonDecoder<P3<A, B, C>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, c)).apply(obj).map(Products::flatten3))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D> JsonDecoder<D> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        F3<A, B, C, D> f) {
        return obj(a, b, c).map(t -> f.f(t._1(), t._2(), t._3()));
    }

    public static <A, B, C, D> JsonDecoder<P4<A, B, C, D>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, d))).apply(obj).map(Products::flatten4))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E> JsonDecoder<E> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        F4<A, B, C, D, E> f) {
        return obj(a, b, c, d).map(t -> f.f(t._1(), t._2(), t._3(), t._4()));
    }

    public static <A, B, C, D, E> JsonDecoder<P5<A, B, C, D, E>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, pair(d, e)))).apply(obj).map(Products::flatten5))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E, FF> JsonDecoder<FF> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        F5<A, B, C, D, E, FF> f) {
        return obj(a, b, c, d, e).map(t -> f.f(t._1(), t._2(), t._3(), t._4(), t._5()));
    }

    public static <A, B, C, D, E, FF> JsonDecoder<P6<A, B, C, D, E, FF>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> f) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, pair(d, pair(e, f))))).apply(obj).map(Products::flatten6))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E, FF, G> JsonDecoder<G> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> ff,
        F6<A, B, C, D, E, FF, G> f) {
        return obj(a, b, c, d, e, ff).map(t -> f.f(t._1(), t._2(), t._3(), t._4(), t._5(), t._6()));
    }

    public static <A, B, C, D, E, FF, G> JsonDecoder<P7<A, B, C, D, E, FF, G>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> f,
        FieldDecoder<G> g) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, pair(d, pair(e, pair(f, g)))))).apply(obj).map(Products::flatten7))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E, FF, G, H> JsonDecoder<H> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> ff,
        FieldDecoder<G> g,
        F7<A, B, C, D, E, FF, G, H> f) {
        return obj(a, b, c, d, e, ff, g).map(t -> f.f(t._1(), t._2(), t._3(), t._4(), t._5(), t._6(), t._7()));
    }

    public static <A, B, C, D, E, FF, G, H> JsonDecoder<P8<A, B, C, D, E, FF, G, H>> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> f,
        FieldDecoder<G> g,
        FieldDecoder<H> h) {
        return v ->
            v
                .onObject(obj -> pair(a, pair(b, pair(c, pair(d, pair(e, pair(f, pair(g, h))))))).apply(obj).map(Products::flatten8))
                .orSome(notAnObjectFailMsg(v));
    }

    public static <A, B, C, D, E, FF, G, H, I> JsonDecoder<I> obj(
        FieldDecoder<A> a,
        FieldDecoder<B> b,
        FieldDecoder<C> c,
        FieldDecoder<D> d,
        FieldDecoder<E> e,
        FieldDecoder<FF> ff,
        FieldDecoder<G> g,
        FieldDecoder<H> h,
        F8<A, B, C, D, E, FF, G, H, I> f) {
        return obj(a, b, c, d, e, ff, g, h).map(t -> f.f(t._1(), t._2(), t._3(), t._4(), t._5(), t._6(), t._7(), t._8()));
    }

    public static <A> JsonDecoder<A> objE(Try1<JsonResult<JsonObject>, A, JsonConversionFailure> f) {
        return value -> {
            JsonResult<JsonObject> jobj =
                value
                    .onObject(map -> success(new JsonObject(map)))
                    .orSome(fail(value + " is not a json object"));
            try {
                return success(f.f(jobj));
            } catch (JsonConversionFailure e) {
                return fail(e.getMessage());
            }
        };
    }

    public static <A> JsonDecoder<A> subclassObjDecoder(
        String selectorFieldName,
        P2<String, JsonDecoder<A>>... decoders) {
        List<P2<String, JsonDecoder<A>>> decoderList = List.arrayList(decoders);
        return v ->
            v.fieldAsText(selectorFieldName).bind(type ->
                decoderList
                    .find(pair -> pair._1().equals(type))
                    .option(
                        fail("No decoder was registererd for type " + type),
                        found -> found._2().decode(v)
                    )
            );
    }

    public interface FieldDecoder<A> {
        JsonResult<A> apply(TreeMap<String, JsonValue> fields);

        default <B> FieldDecoder<B> map(F<A, B> f) {
            return fields -> apply(fields).map(f);
        }

        default FieldDecoder<A> or(FieldDecoder<A> other) {
            return fields -> apply(fields).orResult(() -> other.apply(fields));
        }
    }

    private static <A, B> FieldDecoder<P2<A, B>> pair(FieldDecoder<A> ad, FieldDecoder<B> bd) {
        return fields -> ad.apply(fields).bind(a -> bd.apply(fields).map(b -> p(a, b)));
    }

    public static <A, B> JsonDecoder<P2<A, B>> and(JsonDecoder<A> aDecoder, JsonDecoder<B> bDecoder) {
        return v -> aDecoder.decode(v).bind(a -> bDecoder.decode(v).map(b -> p(a, b)));
    }

    public static <A, B, C> JsonDecoder<C> and(JsonDecoder<A> aDecoder, JsonDecoder<B> bDecoder, F2<A, B, C> join) {
        return and(aDecoder, bDecoder).map(t -> join.f(t._1(), t._2()));
    }

    private static <A> JsonResult<A> notAnObjectFailMsg(JsonValue v) {
        return fail("Tried to decode an object but i got a" + v.toString() + ". Did you use the right decoder?");
    }
} public class JsonEncoders {

    /**
     * Encoder that encodes strings into json strings
     */
    public static final JsonEncoder<String> stringEncoder =
        JsonValues::jString;

    /**
     * Encoder that encodes bigdecimals into json numbers
     */
    public static final JsonEncoder<BigDecimal> bigDecimalEncoder =
        JsonValues::jNum;

    /**
     * Encoder that encodes boolean values
     */
    public static final JsonEncoder<Boolean> boolEncoder =
        JsonValues::jBool;

    /**
     * Encoder that encodes integers.
     */
    public static final JsonEncoder<Integer> integerEncoder =
        bigDecimalEncoder.contramap(BigDecimal::valueOf);

    /**
     * Encoder that encodes optional values.
     * @param aEncoder The encoder that can encode the contents of the optional value
     * @param <A> the type of the optional value
     * @return an encoder that can encode optional values.
     */
    public static <A> JsonEncoder<Option<A>> optionEncoder(JsonEncoder<A> aEncoder) {
        return maybeValue -> maybeValue.option(JsonValues.jNull(), aEncoder::encode);
    }

    /**
     * Encodes a fj.data.List into a json array
     *
     * @param aEncoder the encoder for the elements in the list
     * @param <A> the type of the elements
     * @return an encoder that encodes lists
     */
    public static <A> JsonEncoder<List<A>> arrayEncoder(JsonEncoder<A> aEncoder) {
        return list -> JsonValues.jArray(list.map(aEncoder::encode));
    }

    /**
     * Encodes a jf.data.TreeMap into a json object where the keys in the map are
     * represented as field names, and the values are encoded using the supplied encoder.
     *
     * @param aEncoder The encoder that encodes the values in the map
     * @param <A>      The type of the values in the map
     * @return an encoder that can encode maps
     */
    public static <A> JsonEncoder<TreeMap<String, A>> fieldsEncoder(JsonEncoder<A> aEncoder) {
        return map -> JsonValues.jObj(map.map(aEncoder::encode));
    }

    /**
     * A special encoder that is used when constructing objects. (see obj)
     *
     * @param name The name of the field
     * @param a    The encoder that encodes the value
     * @param <A>  The type of the value
     * @return a special encoder that help construct objects.
     */
    public static <A> FieldEncoder<A> field(String name, JsonEncoder<A> a) {
        return (obj, va) -> obj.withField(name, a.encode(va));
    }

    /**
     * Encodes an domain value into a json object.
     *
     * @param fe  the fieldencoder for that field.
     * @param <A> The type of the value that is encoded as the field value
     * @return an encoder that creates json objects
     */
    public static <A> JsonEncoder<A> obj(FieldEncoder<A> fe) {
        return a -> fe.apply(JsonObject.empty, a);
    }

    /**
     * En encoder that encodes domain values into a json object. You supply a field encoder that encodes a field, and
     * a lambda that converts your domain value into the type of the field converter.
     *
     * @param fe the encoder for the field
     * @param f unwraps the object
     * @param <A> the type of the field
     * @param <X> the type of the object
     * @return an encoder that encodes an object as a json obejct
     */
    public static <A, X> JsonEncoder<X> obj(FieldEncoder<A> fe, F<X, A> f) {
        return obj(fe).contramap(f);
    }

    /**
     * An encoder that encodes a tuple of domain values into a json object. You supply encoders for each field.
     *
     * @param a   The encoder that knows how to encode the first field value
     * @param b   The encoder that knows how to encode the second field value
     * @param <A> The type of the first field value
     * @param <B> The type of the second field value
     * @return an encoder that encodes tuples as a json object
     */
    public static <A, B> JsonEncoder<P2<A, B>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b) {
        return p ->
            and(a, b).apply(JsonObject.empty, p);
    }

    /**
     * An encoder that encodes domain object into a json object. You supply encoders for each field, and a lambda that extracts the values that
     *
     * @param a   The encoder that knows how to encode the first field value
     * @param b   The encoder that knows how to encode the second field value
     * @param <A> The type of the first field value
     * @param <B> The type of the second field value
     * @param <X> The type of the domain object
     * @return An encoder that encodes domain objects of type X into a json object.
     */
    public static <A, B, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        F<X, P2<A, B>> f) {
        return
            obj(a, b).contramap(f);
    }

    public static <A, B, C> JsonEncoder<P3<A, B, C>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c) {
        return t ->
            and(a, and(b, c)).apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        F<X, P3<A, B, C>> f) {
        return
            obj(a, b, c).contramap(f);
    }

    public static <A, B, C, D> JsonEncoder<P4<A, B, C, D>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d) {
        return t ->
            and(a, and(b, and(c, d)))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        F<X, P4<A, B, C, D>> f) {
        return obj(a, b, c, d).contramap(f);
    }

    public static <A, B, C, D, E> JsonEncoder<P5<A, B, C, D, E>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e) {
        return t ->
            and(a, and(b, and(c, and(d, e))))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, E, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        F<X, P5<A, B, C, D, E>> f) {
        return
            obj(a, b, c, d, e).contramap(f);
    }

    public static <A, B, C, D, E, FF> JsonEncoder<P6<A, B, C, D, E, FF>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> f) {
        return t ->
            and(a, and(b, and(c, and(d, and(e, f)))))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, E, FF, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> ff,
        F<X, P6<A, B, C, D, E, FF>> f) {
        return obj(a, b, c, d, e, ff).contramap(f);
    }

    public static <A, B, C, D, E, FF, G> JsonEncoder<P7<A, B, C, D, E, FF, G>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> f,
        FieldEncoder<G> g) {
        return t ->
            and(a, and(b, and(c, and(d, and(e, and(f, g))))))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, E, FF, G, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> ff,
        FieldEncoder<G> g,
        F<X, P7<A, B, C, D, E, FF, G>> f) {
        return obj(a, b, c, d, e, ff, g).contramap(f);
    }

    public static <A, B, C, D, E, FF, G, H> JsonEncoder<P8<A, B, C, D, E, FF, G, H>> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> f,
        FieldEncoder<G> g,
        FieldEncoder<H> h) {
        return t ->
            and(a, and(b, and(c, and(d, and(e, and(f, and(g, h)))))))
                .apply(JsonObject.empty, expand(t));
    }

    public static <A, B, C, D, E, FF, G, H, X> JsonEncoder<X> obj(
        FieldEncoder<A> a,
        FieldEncoder<B> b,
        FieldEncoder<C> c,
        FieldEncoder<D> d,
        FieldEncoder<E> e,
        FieldEncoder<FF> ff,
        FieldEncoder<G> g,
        FieldEncoder<H> h,
        F<X, P8<A, B, C, D, E, FF, G, H>> f) {
        return obj(a, b, c, d, e, ff, g, h).contramap(f);
    }

    public interface FieldEncoder<A> {
        JsonObject apply(JsonObject obj, A a);
    }

    private static <A, B> FieldEncoder<P2<A, B>> and(FieldEncoder<A> fa, FieldEncoder<B> fb) {
        return (obj, t) -> fb.apply(fa.apply(obj, t._1()), t._2());
    }

}
import fj.P;
import org.kantega.kson.JsonResult;
import org.kantega.kson.codec.JsonDecoder;
import org.kantega.kson.codec.JsonDecoders;
import org.kantega.kson.codec.JsonEncoder;
import org.kantega.kson.codec.JsonEncoders;
import org.kantega.kson.parser.JsonParser;
import org.kantega.kson.parser.JsonWriter;
import org.kantega.kson.skinny.SkinnyDecoders;
import org.kantega.kson.skinny.SkinnyDemux;
import org.kantega.kson.skinny.SkinnyEncoders;
import org.kantega.kson.skinny.SkinnyMux;
import static fj.P.p;
import static org.kantega.kson.codec.JsonDecoders.*;
import static org.kantega.kson.codec.JsonEncoders.*;
package org.kantega.kson.example;



public class SkinnyExample {

    interface Msg {
    }

    static class UserId {
        final String value;

        UserId(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "UserId{" +
              "value='" + value + '\'' +
              '}';
        }
    }

    static class AddUser implements Msg {

        final UserId userId;
        final String name;

        AddUser(UserId userId, String name) {
            this.userId = userId;
            this.name = name;
        }

        @Override
        public String toString() {
            return "AddUser{" +
              "userId=" + userId +
              ", name='" + name + '\'' +
              '}';
        }
    }

    static class ItemId {
        final String value;

        ItemId(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "ItemId{" +
              "value='" + value + '\'' +
              '}';
        }
    }

    static class AddItem implements Msg {

        final ItemId itemId;
        final UserId userId;

        AddItem(ItemId itemId, UserId userId) {
            this.itemId = itemId;
            this.userId = userId;
        }

        @Override
        public String toString() {
            return "AddItem{" +
              "itemId=" + itemId +
              ", userId=" + userId +
              '}';
        }
    }

    static JsonEncoder<AddUser> addUserEnc =
      SkinnyEncoders.skinnyEncoder(stringEncoder, stringEncoder, addUser -> p(addUser.userId.value, addUser.name));

    static JsonEncoder<AddItem> addItemEnc =
      SkinnyEncoders.skinnyEncoder(stringEncoder, stringEncoder, addItem -> p(addItem.itemId.value, addItem.userId.value));

    static JsonDecoder<AddUser> addUserDec =
      SkinnyDecoders.skinny(stringDecoder, stringDecoder, (uid, name) -> new AddUser(new UserId(uid), name));

    static JsonDecoder<AddItem> addItemDec =
      SkinnyDecoders.skinny(stringDecoder, stringDecoder, (iid, uid) -> new AddItem(new ItemId(iid), new UserId(uid)));

    public static void main(String[] args) {

        AddUser addUserMsg = new AddUser(new UserId("a"), "Ola Normann");
        AddItem addItemMsg = new AddItem(new ItemId("123qweasd"), new UserId("a"));


        String addUserMsgAsString = JsonWriter.write(addUserEnc.encode(addUserMsg));
        String addItemMsgAsString = JsonWriter.write(addItemEnc.encode(addItemMsg));

        System.out.println(addUserMsgAsString);
        System.out.println(addItemMsgAsString);

        JsonResult<AddUser> addUserDecoded = JsonParser.parse(addUserMsgAsString).decode(addUserDec);